This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ version: 2
jobs:
"server-test":
docker:
- image: circleci/python:3.7-node-browsers
- image: circleci/python:3.7.6-node-browsers
- image: cypress/base:10

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection

### Changed
- [#787](https://github.com/plotly/dash-table/pull/787)
- Clicking on a link in a Markdown cell now requires a single click instead of two
- Links in Markdown cells now open a new tab (target="_blank")

## [4.7.0] - 2020-05-05
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
Expand Down
10 changes: 5 additions & 5 deletions src/dash-table/components/CellMarkdown/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import React, {
import DOM from 'core/browser/DOM';
import { memoizeOne } from 'core/memoizer';

import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
import Markdown from 'dash-table/utils/Markdown';

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

getMarkdown = memoizeOne((value: string, _ready: any) => ({
dangerouslySetInnerHTML: {
__html: MarkdownHighlighter.render(String(value))
__html: Markdown.render(String(value))
}
}));

constructor(props: IProps) {
super(props);

if (MarkdownHighlighter.isReady !== true) {
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
if (Markdown.isReady !== true) {
Markdown.isReady.then(() => { this.setState({}); });
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/EdgeFactory.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ export default class EdgeFactory {
}

private memoizedCreateEdges = memoizeOne((
active_cell: ICellCoordinates,
active_cell: ICellCoordinates | undefined,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing information was inaccurate.

columns: Columns,
visibleColumns: Columns,
operations: number,
Expand Down
18 changes: 10 additions & 8 deletions src/dash-table/components/Table/props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,7 @@ export interface IProps {
tooltip_conditional: ConditionalTooltip[];

active_cell?: ICellCoordinates;
cell_selectable?: boolean;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New optional prop. If false, user can't select cells and active_cell and selected_cells will always be treated as null/empty.

column_selectable?: Selection;
columns?: Columns;
dropdown?: StaticDropdowns;
Expand DownExpand Up@@ -351,35 +352,35 @@ export interface IProps {
}

interface IDefaultProps {
active_cell: ICellCoordinates;
cell_selectable: boolean;
column_selectable: Selection;
css: IStylesheetRule[];
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
css: IStylesheetRule[];
editable: boolean;
end_cell: ICellCoordinates;
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
row_deletable: boolean;
row_selectable: Selection;
selected_cells: SelectedCells;
selected_columns: string[];
start_cell: ICellCoordinates;
end_cell: ICellCoordinates;
selected_rows: Indices;
selected_row_ids: RowId[];
selected_rows: Indices;
sort_action: TableAction;
sort_by: SortBy;
sort_mode: SortMode;
sort_as_null: SortAsNull;
start_cell: ICellCoordinates;
style_as_list_view: boolean;
tooltip_data: DataTooltips;

Expand DownExpand Up@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
active_cell?: ICellCoordinates;
applyFocus?: boolean;
cell_selectable: boolean;
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
Expand Down
7 changes: 7 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ export const defaultProps = {
selected_columns: [],
selected_rows: [],
selected_row_ids: [],
cell_selectable: true,
row_selectable: false,

style_table: {},
Expand DownExpand Up@@ -622,6 +623,12 @@ export const propTypes = {
*/
row_deletable: PropTypes.bool,

/**
* If True (default), then it is possible to click and navigate
* table cells.
*/
cell_selectable: PropTypes.bool,

/**
* If `single`, then the user can select a single row
* via a radio button that will appear next to each row.
Expand Down
14 changes: 13 additions & 1 deletion src/dash-table/dash/Sanitizer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,8 @@ import {
ExportFormat,
ExportHeaders,
IFilterAction,
FilterLogicalOperator
FilterLogicalOperator,
SelectedCells
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand All@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {

const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];

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

Expand DownExpand Up@@ -99,7 +101,16 @@ export default class Sanitizer {
headerFormat = ExportHeaders.Ids;
}

const active_cell = props.cell_selectable ?
props.active_cell :
undefined;

const selected_cells = props.cell_selectable ?
props.selected_cells :
NULL_SELECTED_CELLS;

return R.merge(props, {
active_cell,
columns,
data,
export_headers: headerFormat,
Expand All@@ -108,6 +119,7 @@ export default class Sanitizer {
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
loading_state: dataLoading(props.loading_state),
locale_format,
selected_cells,
visibleColumns
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/wrapperStyles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ const getter = (
styles: IConvertedStyle[],
data: Data,
offset: IViewportOffset,
activeCell: ICellCoordinates,
activeCell: ICellCoordinates | undefined,
selectedCells: SelectedCells
) => {
baseline = shallowClone(baseline);
Expand Down
12 changes: 10 additions & 2 deletions src/dash-table/handlers/cellEvents.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { min, max, set, lensPath } from 'ramda';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
import isActive from 'dash-table/derived/cell/isActive';
import isSelected from 'dash-table/derived/cell/isSelected';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
Expand All@@ -12,6 +12,7 @@ export const handleClick = (
e: any
) => {
const {
cell_selectable,
selected_cells,
active_cell,
setProps,
Expand All@@ -29,7 +30,14 @@ export const handleClick = (
return;
}

e.preventDefault();
const column = visibleColumns[col];
if (column.presentation !== Presentation.Markdown) {
e.preventDefault();
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow click-through to the most nested target element for Markdown cells.


if (!cell_selectable) {
return;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt out of selection processing if the table doesn't have selectable cells.


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

export default class MarkdownHighlighter {
export default class Markdown {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as this helper doesn't only cover highlights now.


static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
MarkdownHighlighter.hljsResolve = resolve;
Markdown.hljsResolve = resolve;
});

static render = (value: string) => {
return MarkdownHighlighter.md.render(value);
return Markdown.md.render(value);
}

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

private static readonly md: Remarkable = new Remarkable({
highlight: (str: string, lang: string) => {
if (MarkdownHighlighter.hljs) {
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
if (Markdown.hljs) {
if (lang && Markdown.hljs.getLanguage(lang)) {
try {
return MarkdownHighlighter.hljs.highlight(lang, str).value;
return Markdown.hljs.highlight(lang, str).value;
} catch (err) { }
}

try {
return MarkdownHighlighter.hljs.highlightAuto(str).value;
return Markdown.hljs.highlightAuto(str).value;
} catch (err) { }
} else {
MarkdownHighlighter.loadhljs();
Markdown.loadhljs();
}
return '';
}
},
linkTarget:'_blank'
});

private static async loadhljs() {
MarkdownHighlighter.hljs = await LazyLoader.hljs;
MarkdownHighlighter.hljsResolve();
MarkdownHighlighter.isReady = true;
Markdown.hljs = await LazyLoader.hljs;
Markdown.hljsResolve();
Markdown.isReady = true;
}
}
9 changes: 0 additions & 9 deletions tests/cypress/tests/standalone/markdown_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,15 +136,6 @@ describe('markdown cells', () => {
});
});

describe('clicking links', () => {
it('correctly redirects', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
// change href, since Cypress raises error when navigating away from localhost
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
cy.url().should('include', `#testlinkclick`);
});
});

describe('loading highlightjs', () => {
it('loads highlight.js and does not attach hljs to window', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
Expand Down
46 changes: 46 additions & 0 deletions tests/selenium/test_markdown_link.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import dash
from dash_table import DataTable
import pytest


def get_app(cell_selectable):
md = "[Click me](https://www.google.com)"

data = [
dict(a=md, b=md),
dict(a=md, b=md),
]

app = dash.Dash(__name__)

app.layout = DataTable(
id="table",
columns=[
dict(name="a", id="a", type="text", presentation="markdown"),
dict(name="b", id="b", type="text", presentation="markdown"),
],
data=data,
cell_selectable=cell_selectable,
)

return app


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
test.start_server(get_app(cell_selectable))

target = test.table("table")

assert len(test.driver.window_handles) == 1
target.cell(0, "a").get().find_element_by_css_selector("a").click()
assert target.cell(0, "a").is_selected() == cell_selectable
assert len(test.driver.window_handles) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I might add here is (1) verify that the new window actually went to google, and (2) switch back to the first window and verify that the cell is selected iff cell_selectable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Make sure the new tab is what's expected
test.driver.switch_to_window(test.driver.window_handles[1])
assert test.driver.current_url.startswith("https://www.google.com")

# Make sure the cell is still selected iff cell_selectable, after switching tabs
test.driver.switch_to_window(test.driver.window_handles[0])
assert target.cell(0, "a").is_selected() == cell_selectable
18 changes: 18 additions & 0 deletions tests/selenium/test_navigation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
test.send_keys(Keys.ARROW_RIGHT)
test.send_keys(Keys.ARROW_DOWN)
assert target.cell(i, i).is_focused()


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_navg005_unselectable_cells(test, cell_selectable):
app = dash.Dash(__name__)
app.layout = DataTable(
id="table",
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
data=[dict(a=0, b=0), dict(a=1, b=2)],
cell_selectable=cell_selectable,
)

test.start_server(app)

target = test.table("table")
target.cell(0, "a").click()

assert target.cell(0, "a").is_selected() == cell_selectable
14 changes: 11 additions & 3 deletions tests/visual/percy-storybook/Style.percy.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>))
.add('unselectable cells', () => (<DataTable
{...DEFAULT_TABLE}
id='unselectable-cells'
cell_selectable={false}
selected_cells={[
{ row: 1, column: 1, column_id: 'b' },
{ row: 1, column: 2, column_id: 'c' },
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>));



, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ version: 2
jobs:
"server-test":
docker:
- image: circleci/python:3.7-node-browsers
- image: circleci/python:3.7.6-node-browsers
- image: cypress/base:10

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection

### Changed
- [#787](https://github.com/plotly/dash-table/pull/787)
- Clicking on a link in a Markdown cell now requires a single click instead of two
- Links in Markdown cells now open a new tab (target="_blank")

## [4.7.0] - 2020-05-05
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
Expand Down
10 changes: 5 additions & 5 deletions src/dash-table/components/CellMarkdown/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import React, {
import DOM from 'core/browser/DOM';
import { memoizeOne } from 'core/memoizer';

import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
import Markdown from 'dash-table/utils/Markdown';

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

getMarkdown = memoizeOne((value: string, _ready: any) => ({
dangerouslySetInnerHTML: {
__html: MarkdownHighlighter.render(String(value))
__html: Markdown.render(String(value))
}
}));

constructor(props: IProps) {
super(props);

if (MarkdownHighlighter.isReady !== true) {
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
if (Markdown.isReady !== true) {
Markdown.isReady.then(() => { this.setState({}); });
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/EdgeFactory.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ export default class EdgeFactory {
}

private memoizedCreateEdges = memoizeOne((
active_cell: ICellCoordinates,
active_cell: ICellCoordinates | undefined,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing information was inaccurate.

columns: Columns,
visibleColumns: Columns,
operations: number,
Expand Down
18 changes: 10 additions & 8 deletions src/dash-table/components/Table/props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,7 @@ export interface IProps {
tooltip_conditional: ConditionalTooltip[];

active_cell?: ICellCoordinates;
cell_selectable?: boolean;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New optional prop. If false, user can't select cells and active_cell and selected_cells will always be treated as null/empty.

column_selectable?: Selection;
columns?: Columns;
dropdown?: StaticDropdowns;
Expand DownExpand Up@@ -351,35 +352,35 @@ export interface IProps {
}

interface IDefaultProps {
active_cell: ICellCoordinates;
cell_selectable: boolean;
column_selectable: Selection;
css: IStylesheetRule[];
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
css: IStylesheetRule[];
editable: boolean;
end_cell: ICellCoordinates;
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
row_deletable: boolean;
row_selectable: Selection;
selected_cells: SelectedCells;
selected_columns: string[];
start_cell: ICellCoordinates;
end_cell: ICellCoordinates;
selected_rows: Indices;
selected_row_ids: RowId[];
selected_rows: Indices;
sort_action: TableAction;
sort_by: SortBy;
sort_mode: SortMode;
sort_as_null: SortAsNull;
start_cell: ICellCoordinates;
style_as_list_view: boolean;
tooltip_data: DataTooltips;

Expand DownExpand Up@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
active_cell?: ICellCoordinates;
applyFocus?: boolean;
cell_selectable: boolean;
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
Expand Down
7 changes: 7 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ export const defaultProps = {
selected_columns: [],
selected_rows: [],
selected_row_ids: [],
cell_selectable: true,
row_selectable: false,

style_table: {},
Expand DownExpand Up@@ -622,6 +623,12 @@ export const propTypes = {
*/
row_deletable: PropTypes.bool,

/**
* If True (default), then it is possible to click and navigate
* table cells.
*/
cell_selectable: PropTypes.bool,

/**
* If `single`, then the user can select a single row
* via a radio button that will appear next to each row.
Expand Down
14 changes: 13 additions & 1 deletion src/dash-table/dash/Sanitizer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,8 @@ import {
ExportFormat,
ExportHeaders,
IFilterAction,
FilterLogicalOperator
FilterLogicalOperator,
SelectedCells
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand All@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {

const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];

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

Expand DownExpand Up@@ -99,7 +101,16 @@ export default class Sanitizer {
headerFormat = ExportHeaders.Ids;
}

const active_cell = props.cell_selectable ?
props.active_cell :
undefined;

const selected_cells = props.cell_selectable ?
props.selected_cells :
NULL_SELECTED_CELLS;

return R.merge(props, {
active_cell,
columns,
data,
export_headers: headerFormat,
Expand All@@ -108,6 +119,7 @@ export default class Sanitizer {
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
loading_state: dataLoading(props.loading_state),
locale_format,
selected_cells,
visibleColumns
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/wrapperStyles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ const getter = (
styles: IConvertedStyle[],
data: Data,
offset: IViewportOffset,
activeCell: ICellCoordinates,
activeCell: ICellCoordinates | undefined,
selectedCells: SelectedCells
) => {
baseline = shallowClone(baseline);
Expand Down
12 changes: 10 additions & 2 deletions src/dash-table/handlers/cellEvents.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { min, max, set, lensPath } from 'ramda';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
import isActive from 'dash-table/derived/cell/isActive';
import isSelected from 'dash-table/derived/cell/isSelected';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
Expand All@@ -12,6 +12,7 @@ export const handleClick = (
e: any
) => {
const {
cell_selectable,
selected_cells,
active_cell,
setProps,
Expand All@@ -29,7 +30,14 @@ export const handleClick = (
return;
}

e.preventDefault();
const column = visibleColumns[col];
if (column.presentation !== Presentation.Markdown) {
e.preventDefault();
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow click-through to the most nested target element for Markdown cells.


if (!cell_selectable) {
return;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt out of selection processing if the table doesn't have selectable cells.


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

export default class MarkdownHighlighter {
export default class Markdown {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as this helper doesn't only cover highlights now.


static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
MarkdownHighlighter.hljsResolve = resolve;
Markdown.hljsResolve = resolve;
});

static render = (value: string) => {
return MarkdownHighlighter.md.render(value);
return Markdown.md.render(value);
}

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

private static readonly md: Remarkable = new Remarkable({
highlight: (str: string, lang: string) => {
if (MarkdownHighlighter.hljs) {
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
if (Markdown.hljs) {
if (lang && Markdown.hljs.getLanguage(lang)) {
try {
return MarkdownHighlighter.hljs.highlight(lang, str).value;
return Markdown.hljs.highlight(lang, str).value;
} catch (err) { }
}

try {
return MarkdownHighlighter.hljs.highlightAuto(str).value;
return Markdown.hljs.highlightAuto(str).value;
} catch (err) { }
} else {
MarkdownHighlighter.loadhljs();
Markdown.loadhljs();
}
return '';
}
},
linkTarget:'_blank'
});

private static async loadhljs() {
MarkdownHighlighter.hljs = await LazyLoader.hljs;
MarkdownHighlighter.hljsResolve();
MarkdownHighlighter.isReady = true;
Markdown.hljs = await LazyLoader.hljs;
Markdown.hljsResolve();
Markdown.isReady = true;
}
}
9 changes: 0 additions & 9 deletions tests/cypress/tests/standalone/markdown_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,15 +136,6 @@ describe('markdown cells', () => {
});
});

describe('clicking links', () => {
it('correctly redirects', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
// change href, since Cypress raises error when navigating away from localhost
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
cy.url().should('include', `#testlinkclick`);
});
});

describe('loading highlightjs', () => {
it('loads highlight.js and does not attach hljs to window', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
Expand Down
46 changes: 46 additions & 0 deletions tests/selenium/test_markdown_link.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import dash
from dash_table import DataTable
import pytest


def get_app(cell_selectable):
md = "[Click me](https://www.google.com)"

data = [
dict(a=md, b=md),
dict(a=md, b=md),
]

app = dash.Dash(__name__)

app.layout = DataTable(
id="table",
columns=[
dict(name="a", id="a", type="text", presentation="markdown"),
dict(name="b", id="b", type="text", presentation="markdown"),
],
data=data,
cell_selectable=cell_selectable,
)

return app


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
test.start_server(get_app(cell_selectable))

target = test.table("table")

assert len(test.driver.window_handles) == 1
target.cell(0, "a").get().find_element_by_css_selector("a").click()
assert target.cell(0, "a").is_selected() == cell_selectable
assert len(test.driver.window_handles) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I might add here is (1) verify that the new window actually went to google, and (2) switch back to the first window and verify that the cell is selected iff cell_selectable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Make sure the new tab is what's expected
test.driver.switch_to_window(test.driver.window_handles[1])
assert test.driver.current_url.startswith("https://www.google.com")

# Make sure the cell is still selected iff cell_selectable, after switching tabs
test.driver.switch_to_window(test.driver.window_handles[0])
assert target.cell(0, "a").is_selected() == cell_selectable
18 changes: 18 additions & 0 deletions tests/selenium/test_navigation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
test.send_keys(Keys.ARROW_RIGHT)
test.send_keys(Keys.ARROW_DOWN)
assert target.cell(i, i).is_focused()


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_navg005_unselectable_cells(test, cell_selectable):
app = dash.Dash(__name__)
app.layout = DataTable(
id="table",
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
data=[dict(a=0, b=0), dict(a=1, b=2)],
cell_selectable=cell_selectable,
)

test.start_server(app)

target = test.table("table")
target.cell(0, "a").click()

assert target.cell(0, "a").is_selected() == cell_selectable
14 changes: 11 additions & 3 deletions tests/visual/percy-storybook/Style.percy.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>))
.add('unselectable cells', () => (<DataTable
{...DEFAULT_TABLE}
id='unselectable-cells'
cell_selectable={false}
selected_cells={[
{ row: 1, column: 1, column_id: 'b' },
{ row: 1, column: 2, column_id: 'c' },
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>));



, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ version: 2
jobs:
"server-test":
docker:
- image: circleci/python:3.7-node-browsers
- image: circleci/python:3.7.6-node-browsers
- image: cypress/base:10

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection

### Changed
- [#787](https://github.com/plotly/dash-table/pull/787)
- Clicking on a link in a Markdown cell now requires a single click instead of two
- Links in Markdown cells now open a new tab (target="_blank")

## [4.7.0] - 2020-05-05
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
Expand Down
10 changes: 5 additions & 5 deletions src/dash-table/components/CellMarkdown/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import React, {
import DOM from 'core/browser/DOM';
import { memoizeOne } from 'core/memoizer';

import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
import Markdown from 'dash-table/utils/Markdown';

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

getMarkdown = memoizeOne((value: string, _ready: any) => ({
dangerouslySetInnerHTML: {
__html: MarkdownHighlighter.render(String(value))
__html: Markdown.render(String(value))
}
}));

constructor(props: IProps) {
super(props);

if (MarkdownHighlighter.isReady !== true) {
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
if (Markdown.isReady !== true) {
Markdown.isReady.then(() => { this.setState({}); });
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/EdgeFactory.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ export default class EdgeFactory {
}

private memoizedCreateEdges = memoizeOne((
active_cell: ICellCoordinates,
active_cell: ICellCoordinates | undefined,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing information was inaccurate.

columns: Columns,
visibleColumns: Columns,
operations: number,
Expand Down
18 changes: 10 additions & 8 deletions src/dash-table/components/Table/props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,7 @@ export interface IProps {
tooltip_conditional: ConditionalTooltip[];

active_cell?: ICellCoordinates;
cell_selectable?: boolean;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New optional prop. If false, user can't select cells and active_cell and selected_cells will always be treated as null/empty.

column_selectable?: Selection;
columns?: Columns;
dropdown?: StaticDropdowns;
Expand DownExpand Up@@ -351,35 +352,35 @@ export interface IProps {
}

interface IDefaultProps {
active_cell: ICellCoordinates;
cell_selectable: boolean;
column_selectable: Selection;
css: IStylesheetRule[];
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
css: IStylesheetRule[];
editable: boolean;
end_cell: ICellCoordinates;
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
row_deletable: boolean;
row_selectable: Selection;
selected_cells: SelectedCells;
selected_columns: string[];
start_cell: ICellCoordinates;
end_cell: ICellCoordinates;
selected_rows: Indices;
selected_row_ids: RowId[];
selected_rows: Indices;
sort_action: TableAction;
sort_by: SortBy;
sort_mode: SortMode;
sort_as_null: SortAsNull;
start_cell: ICellCoordinates;
style_as_list_view: boolean;
tooltip_data: DataTooltips;

Expand DownExpand Up@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
active_cell?: ICellCoordinates;
applyFocus?: boolean;
cell_selectable: boolean;
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
Expand Down
7 changes: 7 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ export const defaultProps = {
selected_columns: [],
selected_rows: [],
selected_row_ids: [],
cell_selectable: true,
row_selectable: false,

style_table: {},
Expand DownExpand Up@@ -622,6 +623,12 @@ export const propTypes = {
*/
row_deletable: PropTypes.bool,

/**
* If True (default), then it is possible to click and navigate
* table cells.
*/
cell_selectable: PropTypes.bool,

/**
* If `single`, then the user can select a single row
* via a radio button that will appear next to each row.
Expand Down
14 changes: 13 additions & 1 deletion src/dash-table/dash/Sanitizer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,8 @@ import {
ExportFormat,
ExportHeaders,
IFilterAction,
FilterLogicalOperator
FilterLogicalOperator,
SelectedCells
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand All@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {

const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];

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

Expand DownExpand Up@@ -99,7 +101,16 @@ export default class Sanitizer {
headerFormat = ExportHeaders.Ids;
}

const active_cell = props.cell_selectable ?
props.active_cell :
undefined;

const selected_cells = props.cell_selectable ?
props.selected_cells :
NULL_SELECTED_CELLS;

return R.merge(props, {
active_cell,
columns,
data,
export_headers: headerFormat,
Expand All@@ -108,6 +119,7 @@ export default class Sanitizer {
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
loading_state: dataLoading(props.loading_state),
locale_format,
selected_cells,
visibleColumns
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/wrapperStyles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ const getter = (
styles: IConvertedStyle[],
data: Data,
offset: IViewportOffset,
activeCell: ICellCoordinates,
activeCell: ICellCoordinates | undefined,
selectedCells: SelectedCells
) => {
baseline = shallowClone(baseline);
Expand Down
12 changes: 10 additions & 2 deletions src/dash-table/handlers/cellEvents.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { min, max, set, lensPath } from 'ramda';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
import isActive from 'dash-table/derived/cell/isActive';
import isSelected from 'dash-table/derived/cell/isSelected';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
Expand All@@ -12,6 +12,7 @@ export const handleClick = (
e: any
) => {
const {
cell_selectable,
selected_cells,
active_cell,
setProps,
Expand All@@ -29,7 +30,14 @@ export const handleClick = (
return;
}

e.preventDefault();
const column = visibleColumns[col];
if (column.presentation !== Presentation.Markdown) {
e.preventDefault();
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow click-through to the most nested target element for Markdown cells.


if (!cell_selectable) {
return;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt out of selection processing if the table doesn't have selectable cells.


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

export default class MarkdownHighlighter {
export default class Markdown {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as this helper doesn't only cover highlights now.


static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
MarkdownHighlighter.hljsResolve = resolve;
Markdown.hljsResolve = resolve;
});

static render = (value: string) => {
return MarkdownHighlighter.md.render(value);
return Markdown.md.render(value);
}

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

private static readonly md: Remarkable = new Remarkable({
highlight: (str: string, lang: string) => {
if (MarkdownHighlighter.hljs) {
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
if (Markdown.hljs) {
if (lang && Markdown.hljs.getLanguage(lang)) {
try {
return MarkdownHighlighter.hljs.highlight(lang, str).value;
return Markdown.hljs.highlight(lang, str).value;
} catch (err) { }
}

try {
return MarkdownHighlighter.hljs.highlightAuto(str).value;
return Markdown.hljs.highlightAuto(str).value;
} catch (err) { }
} else {
MarkdownHighlighter.loadhljs();
Markdown.loadhljs();
}
return '';
}
},
linkTarget:'_blank'
});

private static async loadhljs() {
MarkdownHighlighter.hljs = await LazyLoader.hljs;
MarkdownHighlighter.hljsResolve();
MarkdownHighlighter.isReady = true;
Markdown.hljs = await LazyLoader.hljs;
Markdown.hljsResolve();
Markdown.isReady = true;
}
}
9 changes: 0 additions & 9 deletions tests/cypress/tests/standalone/markdown_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,15 +136,6 @@ describe('markdown cells', () => {
});
});

describe('clicking links', () => {
it('correctly redirects', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
// change href, since Cypress raises error when navigating away from localhost
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
cy.url().should('include', `#testlinkclick`);
});
});

describe('loading highlightjs', () => {
it('loads highlight.js and does not attach hljs to window', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
Expand Down
46 changes: 46 additions & 0 deletions tests/selenium/test_markdown_link.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import dash
from dash_table import DataTable
import pytest


def get_app(cell_selectable):
md = "[Click me](https://www.google.com)"

data = [
dict(a=md, b=md),
dict(a=md, b=md),
]

app = dash.Dash(__name__)

app.layout = DataTable(
id="table",
columns=[
dict(name="a", id="a", type="text", presentation="markdown"),
dict(name="b", id="b", type="text", presentation="markdown"),
],
data=data,
cell_selectable=cell_selectable,
)

return app


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
test.start_server(get_app(cell_selectable))

target = test.table("table")

assert len(test.driver.window_handles) == 1
target.cell(0, "a").get().find_element_by_css_selector("a").click()
assert target.cell(0, "a").is_selected() == cell_selectable
assert len(test.driver.window_handles) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I might add here is (1) verify that the new window actually went to google, and (2) switch back to the first window and verify that the cell is selected iff cell_selectable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Make sure the new tab is what's expected
test.driver.switch_to_window(test.driver.window_handles[1])
assert test.driver.current_url.startswith("https://www.google.com")

# Make sure the cell is still selected iff cell_selectable, after switching tabs
test.driver.switch_to_window(test.driver.window_handles[0])
assert target.cell(0, "a").is_selected() == cell_selectable
18 changes: 18 additions & 0 deletions tests/selenium/test_navigation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
test.send_keys(Keys.ARROW_RIGHT)
test.send_keys(Keys.ARROW_DOWN)
assert target.cell(i, i).is_focused()


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_navg005_unselectable_cells(test, cell_selectable):
app = dash.Dash(__name__)
app.layout = DataTable(
id="table",
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
data=[dict(a=0, b=0), dict(a=1, b=2)],
cell_selectable=cell_selectable,
)

test.start_server(app)

target = test.table("table")
target.cell(0, "a").click()

assert target.cell(0, "a").is_selected() == cell_selectable
14 changes: 11 additions & 3 deletions tests/visual/percy-storybook/Style.percy.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>))
.add('unselectable cells', () => (<DataTable
{...DEFAULT_TABLE}
id='unselectable-cells'
cell_selectable={false}
selected_cells={[
{ row: 1, column: 1, column_id: 'b' },
{ row: 1, column: 2, column_id: 'c' },
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>));



, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ version: 2
jobs:
"server-test":
docker:
- image: circleci/python:3.7-node-browsers
- image: circleci/python:3.7.6-node-browsers
- image: cypress/base:10

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection

### Changed
- [#787](https://github.com/plotly/dash-table/pull/787)
- Clicking on a link in a Markdown cell now requires a single click instead of two
- Links in Markdown cells now open a new tab (target="_blank")

## [4.7.0] - 2020-05-05
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
Expand Down
10 changes: 5 additions & 5 deletions src/dash-table/components/CellMarkdown/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import React, {
import DOM from 'core/browser/DOM';
import { memoizeOne } from 'core/memoizer';

import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
import Markdown from 'dash-table/utils/Markdown';

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

getMarkdown = memoizeOne((value: string, _ready: any) => ({
dangerouslySetInnerHTML: {
__html: MarkdownHighlighter.render(String(value))
__html: Markdown.render(String(value))
}
}));

constructor(props: IProps) {
super(props);

if (MarkdownHighlighter.isReady !== true) {
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
if (Markdown.isReady !== true) {
Markdown.isReady.then(() => { this.setState({}); });
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/EdgeFactory.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ export default class EdgeFactory {
}

private memoizedCreateEdges = memoizeOne((
active_cell: ICellCoordinates,
active_cell: ICellCoordinates | undefined,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing information was inaccurate.

columns: Columns,
visibleColumns: Columns,
operations: number,
Expand Down
18 changes: 10 additions & 8 deletions src/dash-table/components/Table/props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,7 @@ export interface IProps {
tooltip_conditional: ConditionalTooltip[];

active_cell?: ICellCoordinates;
cell_selectable?: boolean;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New optional prop. If false, user can't select cells and active_cell and selected_cells will always be treated as null/empty.

column_selectable?: Selection;
columns?: Columns;
dropdown?: StaticDropdowns;
Expand DownExpand Up@@ -351,35 +352,35 @@ export interface IProps {
}

interface IDefaultProps {
active_cell: ICellCoordinates;
cell_selectable: boolean;
column_selectable: Selection;
css: IStylesheetRule[];
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
css: IStylesheetRule[];
editable: boolean;
end_cell: ICellCoordinates;
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
row_deletable: boolean;
row_selectable: Selection;
selected_cells: SelectedCells;
selected_columns: string[];
start_cell: ICellCoordinates;
end_cell: ICellCoordinates;
selected_rows: Indices;
selected_row_ids: RowId[];
selected_rows: Indices;
sort_action: TableAction;
sort_by: SortBy;
sort_mode: SortMode;
sort_as_null: SortAsNull;
start_cell: ICellCoordinates;
style_as_list_view: boolean;
tooltip_data: DataTooltips;

Expand DownExpand Up@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
active_cell?: ICellCoordinates;
applyFocus?: boolean;
cell_selectable: boolean;
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
Expand Down
7 changes: 7 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ export const defaultProps = {
selected_columns: [],
selected_rows: [],
selected_row_ids: [],
cell_selectable: true,
row_selectable: false,

style_table: {},
Expand DownExpand Up@@ -622,6 +623,12 @@ export const propTypes = {
*/
row_deletable: PropTypes.bool,

/**
* If True (default), then it is possible to click and navigate
* table cells.
*/
cell_selectable: PropTypes.bool,

/**
* If `single`, then the user can select a single row
* via a radio button that will appear next to each row.
Expand Down
14 changes: 13 additions & 1 deletion src/dash-table/dash/Sanitizer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,8 @@ import {
ExportFormat,
ExportHeaders,
IFilterAction,
FilterLogicalOperator
FilterLogicalOperator,
SelectedCells
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand All@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {

const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];

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

Expand DownExpand Up@@ -99,7 +101,16 @@ export default class Sanitizer {
headerFormat = ExportHeaders.Ids;
}

const active_cell = props.cell_selectable ?
props.active_cell :
undefined;

const selected_cells = props.cell_selectable ?
props.selected_cells :
NULL_SELECTED_CELLS;

return R.merge(props, {
active_cell,
columns,
data,
export_headers: headerFormat,
Expand All@@ -108,6 +119,7 @@ export default class Sanitizer {
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
loading_state: dataLoading(props.loading_state),
locale_format,
selected_cells,
visibleColumns
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/wrapperStyles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ const getter = (
styles: IConvertedStyle[],
data: Data,
offset: IViewportOffset,
activeCell: ICellCoordinates,
activeCell: ICellCoordinates | undefined,
selectedCells: SelectedCells
) => {
baseline = shallowClone(baseline);
Expand Down
12 changes: 10 additions & 2 deletions src/dash-table/handlers/cellEvents.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { min, max, set, lensPath } from 'ramda';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
import isActive from 'dash-table/derived/cell/isActive';
import isSelected from 'dash-table/derived/cell/isSelected';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
Expand All@@ -12,6 +12,7 @@ export const handleClick = (
e: any
) => {
const {
cell_selectable,
selected_cells,
active_cell,
setProps,
Expand All@@ -29,7 +30,14 @@ export const handleClick = (
return;
}

e.preventDefault();
const column = visibleColumns[col];
if (column.presentation !== Presentation.Markdown) {
e.preventDefault();
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow click-through to the most nested target element for Markdown cells.


if (!cell_selectable) {
return;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt out of selection processing if the table doesn't have selectable cells.


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

export default class MarkdownHighlighter {
export default class Markdown {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as this helper doesn't only cover highlights now.


static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
MarkdownHighlighter.hljsResolve = resolve;
Markdown.hljsResolve = resolve;
});

static render = (value: string) => {
return MarkdownHighlighter.md.render(value);
return Markdown.md.render(value);
}

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

private static readonly md: Remarkable = new Remarkable({
highlight: (str: string, lang: string) => {
if (MarkdownHighlighter.hljs) {
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
if (Markdown.hljs) {
if (lang && Markdown.hljs.getLanguage(lang)) {
try {
return MarkdownHighlighter.hljs.highlight(lang, str).value;
return Markdown.hljs.highlight(lang, str).value;
} catch (err) { }
}

try {
return MarkdownHighlighter.hljs.highlightAuto(str).value;
return Markdown.hljs.highlightAuto(str).value;
} catch (err) { }
} else {
MarkdownHighlighter.loadhljs();
Markdown.loadhljs();
}
return '';
}
},
linkTarget:'_blank'
});

private static async loadhljs() {
MarkdownHighlighter.hljs = await LazyLoader.hljs;
MarkdownHighlighter.hljsResolve();
MarkdownHighlighter.isReady = true;
Markdown.hljs = await LazyLoader.hljs;
Markdown.hljsResolve();
Markdown.isReady = true;
}
}
9 changes: 0 additions & 9 deletions tests/cypress/tests/standalone/markdown_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,15 +136,6 @@ describe('markdown cells', () => {
});
});

describe('clicking links', () => {
it('correctly redirects', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
// change href, since Cypress raises error when navigating away from localhost
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
cy.url().should('include', `#testlinkclick`);
});
});

describe('loading highlightjs', () => {
it('loads highlight.js and does not attach hljs to window', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
Expand Down
46 changes: 46 additions & 0 deletions tests/selenium/test_markdown_link.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import dash
from dash_table import DataTable
import pytest


def get_app(cell_selectable):
md = "[Click me](https://www.google.com)"

data = [
dict(a=md, b=md),
dict(a=md, b=md),
]

app = dash.Dash(__name__)

app.layout = DataTable(
id="table",
columns=[
dict(name="a", id="a", type="text", presentation="markdown"),
dict(name="b", id="b", type="text", presentation="markdown"),
],
data=data,
cell_selectable=cell_selectable,
)

return app


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
test.start_server(get_app(cell_selectable))

target = test.table("table")

assert len(test.driver.window_handles) == 1
target.cell(0, "a").get().find_element_by_css_selector("a").click()
assert target.cell(0, "a").is_selected() == cell_selectable
assert len(test.driver.window_handles) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I might add here is (1) verify that the new window actually went to google, and (2) switch back to the first window and verify that the cell is selected iff cell_selectable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Make sure the new tab is what's expected
test.driver.switch_to_window(test.driver.window_handles[1])
assert test.driver.current_url.startswith("https://www.google.com")

# Make sure the cell is still selected iff cell_selectable, after switching tabs
test.driver.switch_to_window(test.driver.window_handles[0])
assert target.cell(0, "a").is_selected() == cell_selectable
18 changes: 18 additions & 0 deletions tests/selenium/test_navigation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
test.send_keys(Keys.ARROW_RIGHT)
test.send_keys(Keys.ARROW_DOWN)
assert target.cell(i, i).is_focused()


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_navg005_unselectable_cells(test, cell_selectable):
app = dash.Dash(__name__)
app.layout = DataTable(
id="table",
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
data=[dict(a=0, b=0), dict(a=1, b=2)],
cell_selectable=cell_selectable,
)

test.start_server(app)

target = test.table("table")
target.cell(0, "a").click()

assert target.cell(0, "a").is_selected() == cell_selectable
14 changes: 11 additions & 3 deletions tests/visual/percy-storybook/Style.percy.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>))
.add('unselectable cells', () => (<DataTable
{...DEFAULT_TABLE}
id='unselectable-cells'
cell_selectable={false}
selected_cells={[
{ row: 1, column: 1, column_id: 'b' },
{ row: 1, column: 2, column_id: 'c' },
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>));



, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ version: 2
jobs:
"server-test":
docker:
- image: circleci/python:3.7-node-browsers
- image: circleci/python:3.7.6-node-browsers
- image: cypress/base:10

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection

### Changed
- [#787](https://github.com/plotly/dash-table/pull/787)
- Clicking on a link in a Markdown cell now requires a single click instead of two
- Links in Markdown cells now open a new tab (target="_blank")

## [4.7.0] - 2020-05-05
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
Expand Down
10 changes: 5 additions & 5 deletions src/dash-table/components/CellMarkdown/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import React, {
import DOM from 'core/browser/DOM';
import { memoizeOne } from 'core/memoizer';

import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
import Markdown from 'dash-table/utils/Markdown';

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

getMarkdown = memoizeOne((value: string, _ready: any) => ({
dangerouslySetInnerHTML: {
__html: MarkdownHighlighter.render(String(value))
__html: Markdown.render(String(value))
}
}));

constructor(props: IProps) {
super(props);

if (MarkdownHighlighter.isReady !== true) {
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
if (Markdown.isReady !== true) {
Markdown.isReady.then(() => { this.setState({}); });
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/EdgeFactory.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ export default class EdgeFactory {
}

private memoizedCreateEdges = memoizeOne((
active_cell: ICellCoordinates,
active_cell: ICellCoordinates | undefined,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing information was inaccurate.

columns: Columns,
visibleColumns: Columns,
operations: number,
Expand Down
18 changes: 10 additions & 8 deletions src/dash-table/components/Table/props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,7 @@ export interface IProps {
tooltip_conditional: ConditionalTooltip[];

active_cell?: ICellCoordinates;
cell_selectable?: boolean;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New optional prop. If false, user can't select cells and active_cell and selected_cells will always be treated as null/empty.

column_selectable?: Selection;
columns?: Columns;
dropdown?: StaticDropdowns;
Expand DownExpand Up@@ -351,35 +352,35 @@ export interface IProps {
}

interface IDefaultProps {
active_cell: ICellCoordinates;
cell_selectable: boolean;
column_selectable: Selection;
css: IStylesheetRule[];
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
css: IStylesheetRule[];
editable: boolean;
end_cell: ICellCoordinates;
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
row_deletable: boolean;
row_selectable: Selection;
selected_cells: SelectedCells;
selected_columns: string[];
start_cell: ICellCoordinates;
end_cell: ICellCoordinates;
selected_rows: Indices;
selected_row_ids: RowId[];
selected_rows: Indices;
sort_action: TableAction;
sort_by: SortBy;
sort_mode: SortMode;
sort_as_null: SortAsNull;
start_cell: ICellCoordinates;
style_as_list_view: boolean;
tooltip_data: DataTooltips;

Expand DownExpand Up@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
active_cell?: ICellCoordinates;
applyFocus?: boolean;
cell_selectable: boolean;
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
Expand Down
7 changes: 7 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ export const defaultProps = {
selected_columns: [],
selected_rows: [],
selected_row_ids: [],
cell_selectable: true,
row_selectable: false,

style_table: {},
Expand DownExpand Up@@ -622,6 +623,12 @@ export const propTypes = {
*/
row_deletable: PropTypes.bool,

/**
* If True (default), then it is possible to click and navigate
* table cells.
*/
cell_selectable: PropTypes.bool,

/**
* If `single`, then the user can select a single row
* via a radio button that will appear next to each row.
Expand Down
14 changes: 13 additions & 1 deletion src/dash-table/dash/Sanitizer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,8 @@ import {
ExportFormat,
ExportHeaders,
IFilterAction,
FilterLogicalOperator
FilterLogicalOperator,
SelectedCells
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand All@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {

const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];

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

Expand DownExpand Up@@ -99,7 +101,16 @@ export default class Sanitizer {
headerFormat = ExportHeaders.Ids;
}

const active_cell = props.cell_selectable ?
props.active_cell :
undefined;

const selected_cells = props.cell_selectable ?
props.selected_cells :
NULL_SELECTED_CELLS;

return R.merge(props, {
active_cell,
columns,
data,
export_headers: headerFormat,
Expand All@@ -108,6 +119,7 @@ export default class Sanitizer {
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
loading_state: dataLoading(props.loading_state),
locale_format,
selected_cells,
visibleColumns
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/wrapperStyles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ const getter = (
styles: IConvertedStyle[],
data: Data,
offset: IViewportOffset,
activeCell: ICellCoordinates,
activeCell: ICellCoordinates | undefined,
selectedCells: SelectedCells
) => {
baseline = shallowClone(baseline);
Expand Down
12 changes: 10 additions & 2 deletions src/dash-table/handlers/cellEvents.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { min, max, set, lensPath } from 'ramda';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
import isActive from 'dash-table/derived/cell/isActive';
import isSelected from 'dash-table/derived/cell/isSelected';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
Expand All@@ -12,6 +12,7 @@ export const handleClick = (
e: any
) => {
const {
cell_selectable,
selected_cells,
active_cell,
setProps,
Expand All@@ -29,7 +30,14 @@ export const handleClick = (
return;
}

e.preventDefault();
const column = visibleColumns[col];
if (column.presentation !== Presentation.Markdown) {
e.preventDefault();
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow click-through to the most nested target element for Markdown cells.


if (!cell_selectable) {
return;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt out of selection processing if the table doesn't have selectable cells.


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

export default class MarkdownHighlighter {
export default class Markdown {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as this helper doesn't only cover highlights now.


static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
MarkdownHighlighter.hljsResolve = resolve;
Markdown.hljsResolve = resolve;
});

static render = (value: string) => {
return MarkdownHighlighter.md.render(value);
return Markdown.md.render(value);
}

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

private static readonly md: Remarkable = new Remarkable({
highlight: (str: string, lang: string) => {
if (MarkdownHighlighter.hljs) {
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
if (Markdown.hljs) {
if (lang && Markdown.hljs.getLanguage(lang)) {
try {
return MarkdownHighlighter.hljs.highlight(lang, str).value;
return Markdown.hljs.highlight(lang, str).value;
} catch (err) { }
}

try {
return MarkdownHighlighter.hljs.highlightAuto(str).value;
return Markdown.hljs.highlightAuto(str).value;
} catch (err) { }
} else {
MarkdownHighlighter.loadhljs();
Markdown.loadhljs();
}
return '';
}
},
linkTarget:'_blank'
});

private static async loadhljs() {
MarkdownHighlighter.hljs = await LazyLoader.hljs;
MarkdownHighlighter.hljsResolve();
MarkdownHighlighter.isReady = true;
Markdown.hljs = await LazyLoader.hljs;
Markdown.hljsResolve();
Markdown.isReady = true;
}
}
9 changes: 0 additions & 9 deletions tests/cypress/tests/standalone/markdown_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,15 +136,6 @@ describe('markdown cells', () => {
});
});

describe('clicking links', () => {
it('correctly redirects', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
// change href, since Cypress raises error when navigating away from localhost
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
cy.url().should('include', `#testlinkclick`);
});
});

describe('loading highlightjs', () => {
it('loads highlight.js and does not attach hljs to window', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
Expand Down
46 changes: 46 additions & 0 deletions tests/selenium/test_markdown_link.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import dash
from dash_table import DataTable
import pytest


def get_app(cell_selectable):
md = "[Click me](https://www.google.com)"

data = [
dict(a=md, b=md),
dict(a=md, b=md),
]

app = dash.Dash(__name__)

app.layout = DataTable(
id="table",
columns=[
dict(name="a", id="a", type="text", presentation="markdown"),
dict(name="b", id="b", type="text", presentation="markdown"),
],
data=data,
cell_selectable=cell_selectable,
)

return app


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
test.start_server(get_app(cell_selectable))

target = test.table("table")

assert len(test.driver.window_handles) == 1
target.cell(0, "a").get().find_element_by_css_selector("a").click()
assert target.cell(0, "a").is_selected() == cell_selectable
assert len(test.driver.window_handles) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I might add here is (1) verify that the new window actually went to google, and (2) switch back to the first window and verify that the cell is selected iff cell_selectable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Make sure the new tab is what's expected
test.driver.switch_to_window(test.driver.window_handles[1])
assert test.driver.current_url.startswith("https://www.google.com")

# Make sure the cell is still selected iff cell_selectable, after switching tabs
test.driver.switch_to_window(test.driver.window_handles[0])
assert target.cell(0, "a").is_selected() == cell_selectable
18 changes: 18 additions & 0 deletions tests/selenium/test_navigation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
test.send_keys(Keys.ARROW_RIGHT)
test.send_keys(Keys.ARROW_DOWN)
assert target.cell(i, i).is_focused()


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_navg005_unselectable_cells(test, cell_selectable):
app = dash.Dash(__name__)
app.layout = DataTable(
id="table",
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
data=[dict(a=0, b=0), dict(a=1, b=2)],
cell_selectable=cell_selectable,
)

test.start_server(app)

target = test.table("table")
target.cell(0, "a").click()

assert target.cell(0, "a").is_selected() == cell_selectable
14 changes: 11 additions & 3 deletions tests/visual/percy-storybook/Style.percy.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>))
.add('unselectable cells', () => (<DataTable
{...DEFAULT_TABLE}
id='unselectable-cells'
cell_selectable={false}
selected_cells={[
{ row: 1, column: 1, column_id: 'b' },
{ row: 1, column: 2, column_id: 'c' },
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>));



, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ version: 2
jobs:
"server-test":
docker:
- image: circleci/python:3.7-node-browsers
- image: circleci/python:3.7.6-node-browsers
- image: cypress/base:10

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection

### Changed
- [#787](https://github.com/plotly/dash-table/pull/787)
- Clicking on a link in a Markdown cell now requires a single click instead of two
- Links in Markdown cells now open a new tab (target="_blank")

## [4.7.0] - 2020-05-05
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
Expand Down
10 changes: 5 additions & 5 deletions src/dash-table/components/CellMarkdown/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import React, {
import DOM from 'core/browser/DOM';
import { memoizeOne } from 'core/memoizer';

import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
import Markdown from 'dash-table/utils/Markdown';

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

getMarkdown = memoizeOne((value: string, _ready: any) => ({
dangerouslySetInnerHTML: {
__html: MarkdownHighlighter.render(String(value))
__html: Markdown.render(String(value))
}
}));

constructor(props: IProps) {
super(props);

if (MarkdownHighlighter.isReady !== true) {
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
if (Markdown.isReady !== true) {
Markdown.isReady.then(() => { this.setState({}); });
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/EdgeFactory.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ export default class EdgeFactory {
}

private memoizedCreateEdges = memoizeOne((
active_cell: ICellCoordinates,
active_cell: ICellCoordinates | undefined,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing information was inaccurate.

columns: Columns,
visibleColumns: Columns,
operations: number,
Expand Down
18 changes: 10 additions & 8 deletions src/dash-table/components/Table/props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,7 @@ export interface IProps {
tooltip_conditional: ConditionalTooltip[];

active_cell?: ICellCoordinates;
cell_selectable?: boolean;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New optional prop. If false, user can't select cells and active_cell and selected_cells will always be treated as null/empty.

column_selectable?: Selection;
columns?: Columns;
dropdown?: StaticDropdowns;
Expand DownExpand Up@@ -351,35 +352,35 @@ export interface IProps {
}

interface IDefaultProps {
active_cell: ICellCoordinates;
cell_selectable: boolean;
column_selectable: Selection;
css: IStylesheetRule[];
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
css: IStylesheetRule[];
editable: boolean;
end_cell: ICellCoordinates;
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
row_deletable: boolean;
row_selectable: Selection;
selected_cells: SelectedCells;
selected_columns: string[];
start_cell: ICellCoordinates;
end_cell: ICellCoordinates;
selected_rows: Indices;
selected_row_ids: RowId[];
selected_rows: Indices;
sort_action: TableAction;
sort_by: SortBy;
sort_mode: SortMode;
sort_as_null: SortAsNull;
start_cell: ICellCoordinates;
style_as_list_view: boolean;
tooltip_data: DataTooltips;

Expand DownExpand Up@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
active_cell?: ICellCoordinates;
applyFocus?: boolean;
cell_selectable: boolean;
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
Expand Down
7 changes: 7 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ export const defaultProps = {
selected_columns: [],
selected_rows: [],
selected_row_ids: [],
cell_selectable: true,
row_selectable: false,

style_table: {},
Expand DownExpand Up@@ -622,6 +623,12 @@ export const propTypes = {
*/
row_deletable: PropTypes.bool,

/**
* If True (default), then it is possible to click and navigate
* table cells.
*/
cell_selectable: PropTypes.bool,

/**
* If `single`, then the user can select a single row
* via a radio button that will appear next to each row.
Expand Down
14 changes: 13 additions & 1 deletion src/dash-table/dash/Sanitizer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,8 @@ import {
ExportFormat,
ExportHeaders,
IFilterAction,
FilterLogicalOperator
FilterLogicalOperator,
SelectedCells
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand All@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {

const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];

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

Expand DownExpand Up@@ -99,7 +101,16 @@ export default class Sanitizer {
headerFormat = ExportHeaders.Ids;
}

const active_cell = props.cell_selectable ?
props.active_cell :
undefined;

const selected_cells = props.cell_selectable ?
props.selected_cells :
NULL_SELECTED_CELLS;

return R.merge(props, {
active_cell,
columns,
data,
export_headers: headerFormat,
Expand All@@ -108,6 +119,7 @@ export default class Sanitizer {
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
loading_state: dataLoading(props.loading_state),
locale_format,
selected_cells,
visibleColumns
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/wrapperStyles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ const getter = (
styles: IConvertedStyle[],
data: Data,
offset: IViewportOffset,
activeCell: ICellCoordinates,
activeCell: ICellCoordinates | undefined,
selectedCells: SelectedCells
) => {
baseline = shallowClone(baseline);
Expand Down
12 changes: 10 additions & 2 deletions src/dash-table/handlers/cellEvents.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { min, max, set, lensPath } from 'ramda';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
import isActive from 'dash-table/derived/cell/isActive';
import isSelected from 'dash-table/derived/cell/isSelected';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
Expand All@@ -12,6 +12,7 @@ export const handleClick = (
e: any
) => {
const {
cell_selectable,
selected_cells,
active_cell,
setProps,
Expand All@@ -29,7 +30,14 @@ export const handleClick = (
return;
}

e.preventDefault();
const column = visibleColumns[col];
if (column.presentation !== Presentation.Markdown) {
e.preventDefault();
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow click-through to the most nested target element for Markdown cells.


if (!cell_selectable) {
return;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt out of selection processing if the table doesn't have selectable cells.


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

export default class MarkdownHighlighter {
export default class Markdown {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as this helper doesn't only cover highlights now.


static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
MarkdownHighlighter.hljsResolve = resolve;
Markdown.hljsResolve = resolve;
});

static render = (value: string) => {
return MarkdownHighlighter.md.render(value);
return Markdown.md.render(value);
}

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

private static readonly md: Remarkable = new Remarkable({
highlight: (str: string, lang: string) => {
if (MarkdownHighlighter.hljs) {
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
if (Markdown.hljs) {
if (lang && Markdown.hljs.getLanguage(lang)) {
try {
return MarkdownHighlighter.hljs.highlight(lang, str).value;
return Markdown.hljs.highlight(lang, str).value;
} catch (err) { }
}

try {
return MarkdownHighlighter.hljs.highlightAuto(str).value;
return Markdown.hljs.highlightAuto(str).value;
} catch (err) { }
} else {
MarkdownHighlighter.loadhljs();
Markdown.loadhljs();
}
return '';
}
},
linkTarget:'_blank'
});

private static async loadhljs() {
MarkdownHighlighter.hljs = await LazyLoader.hljs;
MarkdownHighlighter.hljsResolve();
MarkdownHighlighter.isReady = true;
Markdown.hljs = await LazyLoader.hljs;
Markdown.hljsResolve();
Markdown.isReady = true;
}
}
9 changes: 0 additions & 9 deletions tests/cypress/tests/standalone/markdown_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,15 +136,6 @@ describe('markdown cells', () => {
});
});

describe('clicking links', () => {
it('correctly redirects', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
// change href, since Cypress raises error when navigating away from localhost
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
cy.url().should('include', `#testlinkclick`);
});
});

describe('loading highlightjs', () => {
it('loads highlight.js and does not attach hljs to window', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
Expand Down
46 changes: 46 additions & 0 deletions tests/selenium/test_markdown_link.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import dash
from dash_table import DataTable
import pytest


def get_app(cell_selectable):
md = "[Click me](https://www.google.com)"

data = [
dict(a=md, b=md),
dict(a=md, b=md),
]

app = dash.Dash(__name__)

app.layout = DataTable(
id="table",
columns=[
dict(name="a", id="a", type="text", presentation="markdown"),
dict(name="b", id="b", type="text", presentation="markdown"),
],
data=data,
cell_selectable=cell_selectable,
)

return app


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
test.start_server(get_app(cell_selectable))

target = test.table("table")

assert len(test.driver.window_handles) == 1
target.cell(0, "a").get().find_element_by_css_selector("a").click()
assert target.cell(0, "a").is_selected() == cell_selectable
assert len(test.driver.window_handles) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I might add here is (1) verify that the new window actually went to google, and (2) switch back to the first window and verify that the cell is selected iff cell_selectable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Make sure the new tab is what's expected
test.driver.switch_to_window(test.driver.window_handles[1])
assert test.driver.current_url.startswith("https://www.google.com")

# Make sure the cell is still selected iff cell_selectable, after switching tabs
test.driver.switch_to_window(test.driver.window_handles[0])
assert target.cell(0, "a").is_selected() == cell_selectable
18 changes: 18 additions & 0 deletions tests/selenium/test_navigation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
test.send_keys(Keys.ARROW_RIGHT)
test.send_keys(Keys.ARROW_DOWN)
assert target.cell(i, i).is_focused()


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_navg005_unselectable_cells(test, cell_selectable):
app = dash.Dash(__name__)
app.layout = DataTable(
id="table",
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
data=[dict(a=0, b=0), dict(a=1, b=2)],
cell_selectable=cell_selectable,
)

test.start_server(app)

target = test.table("table")
target.cell(0, "a").click()

assert target.cell(0, "a").is_selected() == cell_selectable
14 changes: 11 additions & 3 deletions tests/visual/percy-storybook/Style.percy.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>))
.add('unselectable cells', () => (<DataTable
{...DEFAULT_TABLE}
id='unselectable-cells'
cell_selectable={false}
selected_cells={[
{ row: 1, column: 1, column_id: 'b' },
{ row: 1, column: 2, column_id: 'c' },
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>));



, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ version: 2
jobs:
"server-test":
docker:
- image: circleci/python:3.7-node-browsers
- image: circleci/python:3.7.6-node-browsers
- image: cypress/base:10

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection

### Changed
- [#787](https://github.com/plotly/dash-table/pull/787)
- Clicking on a link in a Markdown cell now requires a single click instead of two
- Links in Markdown cells now open a new tab (target="_blank")

## [4.7.0] - 2020-05-05
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
Expand Down
10 changes: 5 additions & 5 deletions src/dash-table/components/CellMarkdown/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import React, {
import DOM from 'core/browser/DOM';
import { memoizeOne } from 'core/memoizer';

import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
import Markdown from 'dash-table/utils/Markdown';

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

getMarkdown = memoizeOne((value: string, _ready: any) => ({
dangerouslySetInnerHTML: {
__html: MarkdownHighlighter.render(String(value))
__html: Markdown.render(String(value))
}
}));

constructor(props: IProps) {
super(props);

if (MarkdownHighlighter.isReady !== true) {
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
if (Markdown.isReady !== true) {
Markdown.isReady.then(() => { this.setState({}); });
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/EdgeFactory.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ export default class EdgeFactory {
}

private memoizedCreateEdges = memoizeOne((
active_cell: ICellCoordinates,
active_cell: ICellCoordinates | undefined,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing information was inaccurate.

columns: Columns,
visibleColumns: Columns,
operations: number,
Expand Down
18 changes: 10 additions & 8 deletions src/dash-table/components/Table/props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,7 @@ export interface IProps {
tooltip_conditional: ConditionalTooltip[];

active_cell?: ICellCoordinates;
cell_selectable?: boolean;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New optional prop. If false, user can't select cells and active_cell and selected_cells will always be treated as null/empty.

column_selectable?: Selection;
columns?: Columns;
dropdown?: StaticDropdowns;
Expand DownExpand Up@@ -351,35 +352,35 @@ export interface IProps {
}

interface IDefaultProps {
active_cell: ICellCoordinates;
cell_selectable: boolean;
column_selectable: Selection;
css: IStylesheetRule[];
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
css: IStylesheetRule[];
editable: boolean;
end_cell: ICellCoordinates;
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
row_deletable: boolean;
row_selectable: Selection;
selected_cells: SelectedCells;
selected_columns: string[];
start_cell: ICellCoordinates;
end_cell: ICellCoordinates;
selected_rows: Indices;
selected_row_ids: RowId[];
selected_rows: Indices;
sort_action: TableAction;
sort_by: SortBy;
sort_mode: SortMode;
sort_as_null: SortAsNull;
start_cell: ICellCoordinates;
style_as_list_view: boolean;
tooltip_data: DataTooltips;

Expand DownExpand Up@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
active_cell?: ICellCoordinates;
applyFocus?: boolean;
cell_selectable: boolean;
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
Expand Down
7 changes: 7 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ export const defaultProps = {
selected_columns: [],
selected_rows: [],
selected_row_ids: [],
cell_selectable: true,
row_selectable: false,

style_table: {},
Expand DownExpand Up@@ -622,6 +623,12 @@ export const propTypes = {
*/
row_deletable: PropTypes.bool,

/**
* If True (default), then it is possible to click and navigate
* table cells.
*/
cell_selectable: PropTypes.bool,

/**
* If `single`, then the user can select a single row
* via a radio button that will appear next to each row.
Expand Down
14 changes: 13 additions & 1 deletion src/dash-table/dash/Sanitizer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,8 @@ import {
ExportFormat,
ExportHeaders,
IFilterAction,
FilterLogicalOperator
FilterLogicalOperator,
SelectedCells
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand All@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {

const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];

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

Expand DownExpand Up@@ -99,7 +101,16 @@ export default class Sanitizer {
headerFormat = ExportHeaders.Ids;
}

const active_cell = props.cell_selectable ?
props.active_cell :
undefined;

const selected_cells = props.cell_selectable ?
props.selected_cells :
NULL_SELECTED_CELLS;

return R.merge(props, {
active_cell,
columns,
data,
export_headers: headerFormat,
Expand All@@ -108,6 +119,7 @@ export default class Sanitizer {
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
loading_state: dataLoading(props.loading_state),
locale_format,
selected_cells,
visibleColumns
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/wrapperStyles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ const getter = (
styles: IConvertedStyle[],
data: Data,
offset: IViewportOffset,
activeCell: ICellCoordinates,
activeCell: ICellCoordinates | undefined,
selectedCells: SelectedCells
) => {
baseline = shallowClone(baseline);
Expand Down
12 changes: 10 additions & 2 deletions src/dash-table/handlers/cellEvents.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { min, max, set, lensPath } from 'ramda';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
import isActive from 'dash-table/derived/cell/isActive';
import isSelected from 'dash-table/derived/cell/isSelected';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
Expand All@@ -12,6 +12,7 @@ export const handleClick = (
e: any
) => {
const {
cell_selectable,
selected_cells,
active_cell,
setProps,
Expand All@@ -29,7 +30,14 @@ export const handleClick = (
return;
}

e.preventDefault();
const column = visibleColumns[col];
if (column.presentation !== Presentation.Markdown) {
e.preventDefault();
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow click-through to the most nested target element for Markdown cells.


if (!cell_selectable) {
return;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt out of selection processing if the table doesn't have selectable cells.


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

export default class MarkdownHighlighter {
export default class Markdown {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as this helper doesn't only cover highlights now.


static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
MarkdownHighlighter.hljsResolve = resolve;
Markdown.hljsResolve = resolve;
});

static render = (value: string) => {
return MarkdownHighlighter.md.render(value);
return Markdown.md.render(value);
}

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

private static readonly md: Remarkable = new Remarkable({
highlight: (str: string, lang: string) => {
if (MarkdownHighlighter.hljs) {
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
if (Markdown.hljs) {
if (lang && Markdown.hljs.getLanguage(lang)) {
try {
return MarkdownHighlighter.hljs.highlight(lang, str).value;
return Markdown.hljs.highlight(lang, str).value;
} catch (err) { }
}

try {
return MarkdownHighlighter.hljs.highlightAuto(str).value;
return Markdown.hljs.highlightAuto(str).value;
} catch (err) { }
} else {
MarkdownHighlighter.loadhljs();
Markdown.loadhljs();
}
return '';
}
},
linkTarget:'_blank'
});

private static async loadhljs() {
MarkdownHighlighter.hljs = await LazyLoader.hljs;
MarkdownHighlighter.hljsResolve();
MarkdownHighlighter.isReady = true;
Markdown.hljs = await LazyLoader.hljs;
Markdown.hljsResolve();
Markdown.isReady = true;
}
}
9 changes: 0 additions & 9 deletions tests/cypress/tests/standalone/markdown_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,15 +136,6 @@ describe('markdown cells', () => {
});
});

describe('clicking links', () => {
it('correctly redirects', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
// change href, since Cypress raises error when navigating away from localhost
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
cy.url().should('include', `#testlinkclick`);
});
});

describe('loading highlightjs', () => {
it('loads highlight.js and does not attach hljs to window', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
Expand Down
46 changes: 46 additions & 0 deletions tests/selenium/test_markdown_link.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import dash
from dash_table import DataTable
import pytest


def get_app(cell_selectable):
md = "[Click me](https://www.google.com)"

data = [
dict(a=md, b=md),
dict(a=md, b=md),
]

app = dash.Dash(__name__)

app.layout = DataTable(
id="table",
columns=[
dict(name="a", id="a", type="text", presentation="markdown"),
dict(name="b", id="b", type="text", presentation="markdown"),
],
data=data,
cell_selectable=cell_selectable,
)

return app


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
test.start_server(get_app(cell_selectable))

target = test.table("table")

assert len(test.driver.window_handles) == 1
target.cell(0, "a").get().find_element_by_css_selector("a").click()
assert target.cell(0, "a").is_selected() == cell_selectable
assert len(test.driver.window_handles) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I might add here is (1) verify that the new window actually went to google, and (2) switch back to the first window and verify that the cell is selected iff cell_selectable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Make sure the new tab is what's expected
test.driver.switch_to_window(test.driver.window_handles[1])
assert test.driver.current_url.startswith("https://www.google.com")

# Make sure the cell is still selected iff cell_selectable, after switching tabs
test.driver.switch_to_window(test.driver.window_handles[0])
assert target.cell(0, "a").is_selected() == cell_selectable
18 changes: 18 additions & 0 deletions tests/selenium/test_navigation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
test.send_keys(Keys.ARROW_RIGHT)
test.send_keys(Keys.ARROW_DOWN)
assert target.cell(i, i).is_focused()


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_navg005_unselectable_cells(test, cell_selectable):
app = dash.Dash(__name__)
app.layout = DataTable(
id="table",
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
data=[dict(a=0, b=0), dict(a=1, b=2)],
cell_selectable=cell_selectable,
)

test.start_server(app)

target = test.table("table")
target.cell(0, "a").click()

assert target.cell(0, "a").is_selected() == cell_selectable
14 changes: 11 additions & 3 deletions tests/visual/percy-storybook/Style.percy.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>))
.add('unselectable cells', () => (<DataTable
{...DEFAULT_TABLE}
id='unselectable-cells'
cell_selectable={false}
selected_cells={[
{ row: 1, column: 1, column_id: 'b' },
{ row: 1, column: 2, column_id: 'c' },
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>));



, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ version: 2
jobs:
"server-test":
docker:
- image: circleci/python:3.7-node-browsers
- image: circleci/python:3.7.6-node-browsers
- image: cypress/base:10

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection

### Changed
- [#787](https://github.com/plotly/dash-table/pull/787)
- Clicking on a link in a Markdown cell now requires a single click instead of two
- Links in Markdown cells now open a new tab (target="_blank")

## [4.7.0] - 2020-05-05
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
Expand Down
10 changes: 5 additions & 5 deletions src/dash-table/components/CellMarkdown/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import React, {
import DOM from 'core/browser/DOM';
import { memoizeOne } from 'core/memoizer';

import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
import Markdown from 'dash-table/utils/Markdown';

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

getMarkdown = memoizeOne((value: string, _ready: any) => ({
dangerouslySetInnerHTML: {
__html: MarkdownHighlighter.render(String(value))
__html: Markdown.render(String(value))
}
}));

constructor(props: IProps) {
super(props);

if (MarkdownHighlighter.isReady !== true) {
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
if (Markdown.isReady !== true) {
Markdown.isReady.then(() => { this.setState({}); });
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/EdgeFactory.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ export default class EdgeFactory {
}

private memoizedCreateEdges = memoizeOne((
active_cell: ICellCoordinates,
active_cell: ICellCoordinates | undefined,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing information was inaccurate.

columns: Columns,
visibleColumns: Columns,
operations: number,
Expand Down
18 changes: 10 additions & 8 deletions src/dash-table/components/Table/props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,7 @@ export interface IProps {
tooltip_conditional: ConditionalTooltip[];

active_cell?: ICellCoordinates;
cell_selectable?: boolean;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New optional prop. If false, user can't select cells and active_cell and selected_cells will always be treated as null/empty.

column_selectable?: Selection;
columns?: Columns;
dropdown?: StaticDropdowns;
Expand DownExpand Up@@ -351,35 +352,35 @@ export interface IProps {
}

interface IDefaultProps {
active_cell: ICellCoordinates;
cell_selectable: boolean;
column_selectable: Selection;
css: IStylesheetRule[];
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
css: IStylesheetRule[];
editable: boolean;
end_cell: ICellCoordinates;
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
row_deletable: boolean;
row_selectable: Selection;
selected_cells: SelectedCells;
selected_columns: string[];
start_cell: ICellCoordinates;
end_cell: ICellCoordinates;
selected_rows: Indices;
selected_row_ids: RowId[];
selected_rows: Indices;
sort_action: TableAction;
sort_by: SortBy;
sort_mode: SortMode;
sort_as_null: SortAsNull;
start_cell: ICellCoordinates;
style_as_list_view: boolean;
tooltip_data: DataTooltips;

Expand DownExpand Up@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
active_cell?: ICellCoordinates;
applyFocus?: boolean;
cell_selectable: boolean;
dropdown: StaticDropdowns;
dropdown_conditional: ConditionalDropdowns;
dropdown_data: DataDropdowns;
Expand Down
7 changes: 7 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ export const defaultProps = {
selected_columns: [],
selected_rows: [],
selected_row_ids: [],
cell_selectable: true,
row_selectable: false,

style_table: {},
Expand DownExpand Up@@ -622,6 +623,12 @@ export const propTypes = {
*/
row_deletable: PropTypes.bool,

/**
* If True (default), then it is possible to click and navigate
* table cells.
*/
cell_selectable: PropTypes.bool,

/**
* If `single`, then the user can select a single row
* via a radio button that will appear next to each row.
Expand Down
14 changes: 13 additions & 1 deletion src/dash-table/dash/Sanitizer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,8 @@ import {
ExportFormat,
ExportHeaders,
IFilterAction,
FilterLogicalOperator
FilterLogicalOperator,
SelectedCells
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand All@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {

const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];

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

Expand DownExpand Up@@ -99,7 +101,16 @@ export default class Sanitizer {
headerFormat = ExportHeaders.Ids;
}

const active_cell = props.cell_selectable ?
props.active_cell :
undefined;

const selected_cells = props.cell_selectable ?
props.selected_cells :
NULL_SELECTED_CELLS;

return R.merge(props, {
active_cell,
columns,
data,
export_headers: headerFormat,
Expand All@@ -108,6 +119,7 @@ export default class Sanitizer {
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
loading_state: dataLoading(props.loading_state),
locale_format,
selected_cells,
visibleColumns
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/wrapperStyles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ const getter = (
styles: IConvertedStyle[],
data: Data,
offset: IViewportOffset,
activeCell: ICellCoordinates,
activeCell: ICellCoordinates | undefined,
selectedCells: SelectedCells
) => {
baseline = shallowClone(baseline);
Expand Down
12 changes: 10 additions & 2 deletions src/dash-table/handlers/cellEvents.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { min, max, set, lensPath } from 'ramda';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
import isActive from 'dash-table/derived/cell/isActive';
import isSelected from 'dash-table/derived/cell/isSelected';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
Expand All@@ -12,6 +12,7 @@ export const handleClick = (
e: any
) => {
const {
cell_selectable,
selected_cells,
active_cell,
setProps,
Expand All@@ -29,7 +30,14 @@ export const handleClick = (
return;
}

e.preventDefault();
const column = visibleColumns[col];
if (column.presentation !== Presentation.Markdown) {
e.preventDefault();
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow click-through to the most nested target element for Markdown cells.


if (!cell_selectable) {
return;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt out of selection processing if the table doesn't have selectable cells.


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

export default class MarkdownHighlighter {
export default class Markdown {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as this helper doesn't only cover highlights now.


static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
MarkdownHighlighter.hljsResolve = resolve;
Markdown.hljsResolve = resolve;
});

static render = (value: string) => {
return MarkdownHighlighter.md.render(value);
return Markdown.md.render(value);
}

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

private static readonly md: Remarkable = new Remarkable({
highlight: (str: string, lang: string) => {
if (MarkdownHighlighter.hljs) {
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
if (Markdown.hljs) {
if (lang && Markdown.hljs.getLanguage(lang)) {
try {
return MarkdownHighlighter.hljs.highlight(lang, str).value;
return Markdown.hljs.highlight(lang, str).value;
} catch (err) { }
}

try {
return MarkdownHighlighter.hljs.highlightAuto(str).value;
return Markdown.hljs.highlightAuto(str).value;
} catch (err) { }
} else {
MarkdownHighlighter.loadhljs();
Markdown.loadhljs();
}
return '';
}
},
linkTarget:'_blank'
});

private static async loadhljs() {
MarkdownHighlighter.hljs = await LazyLoader.hljs;
MarkdownHighlighter.hljsResolve();
MarkdownHighlighter.isReady = true;
Markdown.hljs = await LazyLoader.hljs;
Markdown.hljsResolve();
Markdown.isReady = true;
}
}
9 changes: 0 additions & 9 deletions tests/cypress/tests/standalone/markdown_test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,15 +136,6 @@ describe('markdown cells', () => {
});
});

describe('clicking links', () => {
it('correctly redirects', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
// change href, since Cypress raises error when navigating away from localhost
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
cy.url().should('include', `#testlinkclick`);
});
});

describe('loading highlightjs', () => {
it('loads highlight.js and does not attach hljs to window', () => {
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
Expand Down
46 changes: 46 additions & 0 deletions tests/selenium/test_markdown_link.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import dash
from dash_table import DataTable
import pytest


def get_app(cell_selectable):
md = "[Click me](https://www.google.com)"

data = [
dict(a=md, b=md),
dict(a=md, b=md),
]

app = dash.Dash(__name__)

app.layout = DataTable(
id="table",
columns=[
dict(name="a", id="a", type="text", presentation="markdown"),
dict(name="b", id="b", type="text", presentation="markdown"),
],
data=data,
cell_selectable=cell_selectable,
)

return app


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
test.start_server(get_app(cell_selectable))

target = test.table("table")

assert len(test.driver.window_handles) == 1
target.cell(0, "a").get().find_element_by_css_selector("a").click()
assert target.cell(0, "a").is_selected() == cell_selectable
assert len(test.driver.window_handles) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I might add here is (1) verify that the new window actually went to google, and (2) switch back to the first window and verify that the cell is selected iff cell_selectable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Make sure the new tab is what's expected
test.driver.switch_to_window(test.driver.window_handles[1])
assert test.driver.current_url.startswith("https://www.google.com")

# Make sure the cell is still selected iff cell_selectable, after switching tabs
test.driver.switch_to_window(test.driver.window_handles[0])
assert target.cell(0, "a").is_selected() == cell_selectable
18 changes: 18 additions & 0 deletions tests/selenium/test_navigation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
test.send_keys(Keys.ARROW_RIGHT)
test.send_keys(Keys.ARROW_DOWN)
assert target.cell(i, i).is_focused()


@pytest.mark.parametrize("cell_selectable", [True, False])
def test_navg005_unselectable_cells(test, cell_selectable):
app = dash.Dash(__name__)
app.layout = DataTable(
id="table",
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
data=[dict(a=0, b=0), dict(a=1, b=2)],
cell_selectable=cell_selectable,
)

test.start_server(app)

target = test.table("table")
target.cell(0, "a").click()

assert target.cell(0, "a").is_selected() == cell_selectable
14 changes: 11 additions & 3 deletions tests/visual/percy-storybook/Style.percy.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>))
.add('unselectable cells', () => (<DataTable
{...DEFAULT_TABLE}
id='unselectable-cells'
cell_selectable={false}
selected_cells={[
{ row: 1, column: 1, column_id: 'b' },
{ row: 1, column: 2, column_id: 'c' },
{ row: 2, column: 1, column_id: 'b' },
{ row: 2, column: 2, column_id: 'c' }]}
active_cell={{ row: 1, column: 1 }}
/>));