Skip to content

Commit aceddf3

Browse files
Issue 649 - Fix mismatched row height for fixed columns (#722)
1 parent 965f7ed commit aceddf3

36 files changed

+1212
-788
lines changed

packages/dash-table/.circleci/config.yml

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

99
steps:

packages/dash-table/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## [Unreleased]
6+
### Fixed
7+
- [#722](https://github.com/plotly/dash-table/pull/722) Fix a bug where row height is misaligned when using fixed_columns and/or fixed_rows
8+
59
## [4.6.2] - 2020-04-01
610
### Changed
711
- [#713](https://github.com/plotly/dash-table/pull/713) Update from React 16.8.6 to 16.13.0

packages/dash-table/demo/data.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,15 @@ export const generateMixedMarkdownMockData = (rows: number) => unpackIntoColumns
176176
name: ['Markdown'],
177177
type: ColumnType.Text,
178178
presentation: 'markdown',
179-
data: gendata(_ => [
179+
data: gendata(i => [
180180
'```javascript',
181-
'console.warn("this is a markdown cell")',
181+
...(i % 2 === 0 ?
182+
['console.warn("this is a markdown cell")'] :
183+
[
184+
'console.log("logging things")',
185+
'console.warn("this is a markdown cell")'
186+
]
187+
),
182188
'```'].join('\n'), rows)
183189
},
184190
{

packages/dash-table/src/dash-table/components/Cell/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default class Cell extends Component<ICellProps> {
2222
render() {
2323
const {
2424
attributes,
25-
classes,
25+
className,
2626
onClick,
2727
onDoubleClick,
2828
onMouseEnter,
@@ -35,7 +35,7 @@ export default class Cell extends Component<ICellProps> {
3535
ref='td'
3636
children={(this as any).props.children}
3737
tabIndex={-1}
38-
className={classes}
38+
className={className}
3939
onClick={onClick}
4040
onDoubleClick={onDoubleClick}
4141
onMouseEnter={onMouseEnter}

packages/dash-table/src/dash-table/components/Cell/props.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface IAttributes {
77
export interface ICellProps {
88
active: boolean;
99
attributes: IAttributes;
10-
classes: string;
10+
className: string;
1111
onClick: (e: MouseEvent) => void;
1212
onDoubleClick: (e: MouseEvent) => void;
1313
onMouseEnter: (e: MouseEvent) => void;

packages/dash-table/src/dash-table/components/CellDropdown/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default class CellDropdown extends PureComponent<IProps> {
7575
if (applyFocus && dropdown && document.activeElement !== dropdown) {
7676
// Limitation. If React >= 16 --> Use React.createRef instead to pass parent ref to child
7777
const tdParent = DOM.getFirstParentOfType(dropdown.wrapper, 'td');
78-
if (tdParent) {
78+
if (tdParent && tdParent.className.indexOf('phantom-cell') === -1) {
7979
tdParent.focus();
8080
}
8181
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default class CellMarkdown extends PureComponent<IProps, {}> {
6262
if (applyFocus && el && document.activeElement !== el) {
6363
// Limitation. If React >= 16 --> Use React.createRef instead to pass parent ref to child
6464
const tdParent = DOM.getFirstParentOfType(el, 'td');
65-
if (tdParent) {
65+
if (tdParent && tdParent.className.indexOf('phantom-cell') !== -1) {
6666
tdParent.focus();
6767
}
6868
}

packages/dash-table/src/dash-table/components/ControlledTable/index.tsx

Lines changed: 102 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ const INNER_STYLE = {
4848
minWidth: '100%'
4949
};
5050

51+
const WIDTH_EPSILON = 0.5;
52+
const MAX_WIDTH_ITERATIONS = 30;
53+
5154
export default class ControlledTable extends PureComponent<ControlledTableProps> {
5255
private readonly menuRef = React.createRef<HTMLDivElement>();
5356
private readonly stylesheet: Stylesheet = new Stylesheet(`#${this.props.id}`);
@@ -134,7 +137,7 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
134137
setProps({ active_cell: selected_cells[0] });
135138
}
136139

137-
this.applyStyle();
140+
this.updateUiViewport();
138141
this.handleResize();
139142
}
140143

@@ -146,7 +149,7 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
146149

147150
componentDidUpdate() {
148151
this.updateStylesheet();
149-
this.applyStyle();
152+
this.updateUiViewport();
150153
this.handleResize();
151154
this.handleDropdown();
152155
this.adjustTooltipPosition();
@@ -226,6 +229,8 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
226229

227230
handleResize = (force: boolean = false) => {
228231
const {
232+
fixed_columns,
233+
fixed_rows,
229234
forcedResizeOnly,
230235
setState
231236
} = this.props;
@@ -244,6 +249,7 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
244249

245250
const { r0c0, r0c1, r1c0, r1c1 } = this.refs as { [key: string]: HTMLElement };
246251

252+
247253
// Adjust [fixed columns/fixed rows combo] to fixed rows height
248254
let trs = r0c1.querySelectorAll('tr');
249255
Array.from(r0c0.querySelectorAll('tr')).forEach((tr, index) => {
@@ -261,12 +267,77 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
261267
tr.style.height = getComputedStyle(tr2).height;
262268
});
263269

264-
// Adjust fixed columns data to data height
265-
const contentTd = r1c1.querySelector('tr > td:first-of-type');
266-
if (contentTd) {
267-
const contentTr = contentTd.parentElement as HTMLElement;
270+
if (fixed_columns) {
271+
const r1c0Table = r1c0.querySelector('table') as HTMLElement;
272+
const r1c1Table = r1c0.querySelector('table') as HTMLElement;
273+
274+
r1c0Table.style.width = getComputedStyle(r1c1Table).width;
275+
276+
const lastVisibleTd = r1c0.querySelector(`tr:first-of-type > *:nth-of-type(${fixed_columns})`);
277+
278+
let it = 0;
279+
let currentWidth = r1c0.getBoundingClientRect().width;
280+
let lastWidth = currentWidth;
281+
282+
do {
283+
lastWidth = currentWidth
284+
285+
// Force first column containers width to match visible portion of table
286+
if (lastVisibleTd) {
287+
const r1c0FragmentBounds = r1c0.getBoundingClientRect();
288+
const lastTdBounds = lastVisibleTd.getBoundingClientRect();
289+
currentWidth = lastTdBounds.right - r1c0FragmentBounds.left;
290+
291+
const width = `${currentWidth}px`;
292+
293+
r0c0.style.width = width;
294+
r1c0.style.width = width;
295+
}
296+
297+
// Force second column containers width to match visible portion of table
298+
const firstVisibleTd = r1c1.querySelector(`tr:first-of-type > *:nth-of-type(${fixed_columns + 1})`);
299+
if (firstVisibleTd) {
300+
const r1c1FragmentBounds = r1c1.getBoundingClientRect();
301+
const firstTdBounds = firstVisibleTd.getBoundingClientRect();
302+
303+
const width = firstTdBounds.left - r1c1FragmentBounds.left;
304+
r0c1.style.marginLeft = `-${width}px`;
305+
r0c1.style.marginRight = `${width}px`;
306+
r1c1.style.marginLeft = `-${width}px`;
307+
r1c1.style.marginRight = `${width}px`;
308+
}
268309

269-
this.stylesheet.setRule('.dash-fixed-column tr', `height: ${getComputedStyle(contentTr).height};`);
310+
it++;
311+
} while (
312+
Math.abs(currentWidth - lastWidth) > WIDTH_EPSILON ||
313+
it < MAX_WIDTH_ITERATIONS
314+
)
315+
}
316+
317+
if (fixed_columns || fixed_rows) {
318+
const r1c0CellWidths = Array.from(
319+
r1c0.querySelectorAll('table.cell-table > tbody > tr:first-of-type > *')
320+
).map(c => c.getBoundingClientRect().width);
321+
322+
const r1c1CellWidths = Array.from(
323+
r1c1.querySelectorAll('table.cell-table > tbody > tr:first-of-type > *')
324+
).map(c => c.getBoundingClientRect().width);
325+
326+
Array.from<HTMLElement>(
327+
r0c0.querySelectorAll('table.cell-table > tbody > tr:first-of-type > *')
328+
).forEach((c, i) => this.setCellWidth(c, r1c0CellWidths[i]));
329+
330+
Array.from<HTMLElement>(
331+
r0c0.querySelectorAll('table.cell-table > tbody > tr:last-of-type > *')
332+
).forEach((c, i) => this.setCellWidth(c, r1c0CellWidths[i]));
333+
334+
Array.from<HTMLElement>(
335+
r0c1.querySelectorAll('table.cell-table > tbody > tr:first-of-type > *')
336+
).forEach((c, i) => this.setCellWidth(c, r1c1CellWidths[i]));
337+
338+
Array.from<HTMLElement>(
339+
r0c1.querySelectorAll('table.cell-table > tbody > tr:last-of-type > *')
340+
).forEach((c, i) => this.setCellWidth(c, r1c1CellWidths[i]));
270341
}
271342
}
272343

@@ -627,98 +698,20 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
627698
) || page_action === TableAction.Custom;
628699
}
629700

630-
applyStyle = () => {
631-
const {
632-
fixed_columns,
633-
fixed_rows,
634-
row_deletable,
635-
row_selectable
636-
} = this.props;
637-
638-
const { r1c0, r1c1 } = this.refs as { [key: string]: HTMLElement };
639-
640-
this.updateUiViewport();
641-
642-
if (row_deletable) {
643-
this.stylesheet.setRule(
644-
`.dash-spreadsheet-inner td.dash-delete-cell`,
645-
`width: 30px; max-width: 30px; min-width: 30px;`
646-
);
647-
}
648-
649-
if (row_selectable) {
650-
this.stylesheet.setRule(
651-
`.dash-spreadsheet-inner td.dash-select-cell`,
652-
`width: 30px; max-width: 30px; min-width: 30px;`
653-
);
654-
}
655-
656-
// Adjust the width of the fixed row header
657-
if (fixed_rows) {
658-
Array.from(r1c1.querySelectorAll('tr:first-of-type td.dash-cell, tr:first-of-type th.dash-header')).forEach(td => {
659-
const classname = td.className.split(' ')[1];
660-
const style = getComputedStyle(td);
661-
const width = style.width;
662-
663-
this.stylesheet.setRule(
664-
`.dash-fixed-row:not(.dash-fixed-column) th.${classname}`,
665-
`width: ${width} !important; min-width: ${width} !important; max-width: ${width} !important;`
666-
);
667-
});
668-
}
669-
670-
// Adjust the width of the fixed row / fixed columns header
671-
if (fixed_columns && fixed_rows) {
672-
Array.from(r1c0.querySelectorAll('tr:first-of-type td.dash-cell, tr:first-of-type th.dash-header')).forEach(td => {
673-
const classname = td.className.split(' ')[1];
674-
const style = getComputedStyle(td);
675-
const width = style.width;
676-
677-
this.stylesheet.setRule(
678-
`.dash-fixed-column.dash-fixed-row th.${classname}`,
679-
`width: ${width} !important; min-width: ${width} !important; max-width: ${width} !important;`
680-
);
681-
});
682-
}
683-
684-
// Adjust widths of row deletable/row selectable headers
685-
const subTable = fixed_rows && !fixed_columns ? r1c1 : r1c0;
686-
687-
if (row_deletable) {
688-
Array.from(subTable.querySelectorAll('tr:first-of-type td.dash-delete-cell')).forEach(td => {
689-
const style = getComputedStyle(td);
690-
const width = style.width;
691-
692-
this.stylesheet.setRule(
693-
'th.dash-delete-header',
694-
`width: ${width} !important; min-width: ${width} !important; max-width: ${width} !important;`
695-
);
696-
});
697-
}
698-
if (row_selectable) {
699-
Array.from(subTable.querySelectorAll('tr:first-of-type td.dash-select-cell')).forEach(td => {
700-
const style = getComputedStyle(td);
701-
const width = style.width;
702-
703-
this.stylesheet.setRule(
704-
'th.dash-select-header',
705-
`width: ${width} !important; min-width: ${width} !important; max-width: ${width} !important;`
706-
);
707-
});
708-
}
709-
}
710-
711701
handleDropdown = () => {
712702
const { r1c1 } = this.refs as { [key: string]: HTMLElement };
713703

714704
dropdownHelper(r1c1.querySelector('.Select-menu-outer'));
715705
}
716706

717707
onScroll = (ev: any) => {
718-
const { r0c1 } = this.refs as { [key: string]: HTMLElement };
708+
const { r0c0, r0c1 } = this.refs as { [key: string]: HTMLElement };
719709

720710
Logger.trace(`ControlledTable fragment scrolled to (left,top)=(${ev.target.scrollLeft},${ev.target.scrollTop})`);
721-
r0c1.style.marginLeft = `${-ev.target.scrollLeft}px`;
711+
712+
const margin = parseFloat(ev.target.scrollLeft) + parseFloat(r0c0.style.width);
713+
714+
r0c1.style.marginLeft = `${-margin}px`;
722715

723716
this.updateUiViewport();
724717
this.handleDropdown();
@@ -952,6 +945,25 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
952945
}
953946
}
954947

948+
private setCellWidth(cell: HTMLElement, width: number) {
949+
cell.style.width = `${width}px`;
950+
cell.style.minWidth = `${width}px`;
951+
cell.style.maxWidth = `${width}px`;
952+
cell.style.boxSizing = 'border-box';
953+
954+
/**
955+
* Some browsers handle `th` and `td` size inconsistently.
956+
* Checking the size delta and adjusting for it (different handling of padding and borders)
957+
* allows the table to make sure all sections are correctly aligned.
958+
*/
959+
const delta = cell.getBoundingClientRect().width - width;
960+
if (delta) {
961+
cell.style.width = `${width - delta}px`;
962+
cell.style.minWidth = `${width - delta}px`;
963+
cell.style.maxWidth = `${width - delta}px`;
964+
}
965+
}
966+
955967
private get showToggleColumns(): boolean {
956968
const {
957969
columns,

packages/dash-table/src/dash-table/components/Filter/Column.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper';
88
type SetFilter = (ev: any) => void;
99

1010
interface IColumnFilterProps {
11-
classes: string;
11+
className: string;
1212
columnId: ColumnId;
1313
isValid: boolean;
1414
setFilter: SetFilter;
@@ -39,15 +39,15 @@ export default class ColumnFilter extends PureComponent<IColumnFilterProps, ISta
3939

4040
render() {
4141
const {
42-
classes,
42+
className,
4343
columnId,
4444
isValid,
4545
style,
4646
value
4747
} = this.props;
4848

4949
return (<th
50-
className={classes + (isValid ? '' : ' invalid')}
50+
className={className + (isValid ? '' : ' invalid')}
5151
data-dash-column={columnId}
5252
style={style}
5353
>

packages/dash-table/src/dash-table/components/FilterFactory.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default class FilterFactory {
5050

5151
return (<ColumnFilter
5252
key={`column-${index}`}
53-
classes={`dash-filter column-${index}`}
53+
className={`dash-filter column-${index}`}
5454
columnId={column.id}
5555
isValid={!ast || ast.isValid}
5656
setFilter={this.onChange.bind(this, column, map, setFilter)}

0 commit comments

Comments
 (0)