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

Issue 777 - Align column headers when using fixed rows #793

Merged
merged 34 commits into from
Jun 17, 2020

Conversation

Marc-Andre-Rivet
Copy link
Contributor

@Marc-Andre-Rivet Marc-Andre-Rivet commented Jun 11, 2020

Fixes #777
Fixes #780

Under certain circumstances, the table fragments were misaligned for scenarios involving fixed_rows and fixed_columns. Also, some visual tests were already showing that the behavior was incorrect but it was hard to spot unless zooming in. For that reason I believe the tests already appropriately cover the cases for this PR and that no additional tests are required, only more caution.

A few problems were encountered:

  1. Certain browsers require both the tr and the th rows to have their size explictly set when forcing them through styles. This happens for fixed row/columns using the data nested field. For example, in Firefox, forcing a tr to have a width of 200px can be overriden by a previous th requiring more space. The th "wins". The fix for this is to both set the tr and th cell size for the last row of each group.

  2. The implementation was subtly wrong, using the size of the wrong fragment to coerce another. I think some of those is due to historical reasons, and were not reworked correctly when the table was reworked to allow variable row heights in Issue 649 - Fix mismatched row height for fixed columns #722.

  3. Certain browsers, for certain tables will not be able to converge on a single table width, but will instead oscillate between two very close values. As far as I know always distant by 1px. The "iterations" code for the table now checks for cycles.


Also, a nice side-effect of this PR is that tables with fixed columns now behave more consistently vs. unfixed ones. Will need to check the documentation for any mention of incorrect width that will have to be removed.
image
now looks like this fixed and unfixed:
image

@rpkyle rpkyle changed the title [WIP] Issue 777 [WIP] Align column headers when using fixed rows Jun 11, 2020
@Marc-Andre-Rivet Marc-Andre-Rivet changed the title [WIP] Align column headers when using fixed rows [WIP] Issue 777 - Align column headers when using fixed rows Jun 11, 2020
// For some reason, some browsers require the size to be set explicitly on the header cells too
Array.from<HTMLElement>(
lastTrOfThs.children as any
).forEach((c, i) => this.setCellWidth(c, widths[i]));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Firefox requires th cells to be fixed alongside tr cells for the table to behave consistently.

@@ -33,7 +33,7 @@
"private::test.python": "python -m unittest tests/unit/format_test.py",
"private::test.unit": "cypress run --browser chrome --spec 'tests/cypress/tests/unit/**/*'",
"private::test.standalone": "cypress run --browser chrome --spec 'tests/cypress/tests/standalone/**/*'",
"build.watch": "webpack-dev-server --content-base dash_table --mode development --config webpack.dev.config.js",
"build.watch": "webpack-dev-server --disable-host-check --content-base dash_table --mode development --config webpack.dev.config.js",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In Browserstack, when running on iOS devices, localhost is redirected to bs-local which webpack-dev-server doesn't like, this flag, as its name indicates, makes it not care

r0c1.style.marginLeft = `-${width}px`;
r0c1.style.marginRight = `${width}px`;
r1c1.style.marginLeft = `-${width}px`;
r1c1.style.marginRight = `${width}px`;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

marginRight caused the table to be smaller than expected with fixed columns. Removing it for both r0c1 and r1c1 fragments ensures the table is the expected size

// If the table was resized and isn't in a cycle, re-run `handleResize`.
// If the final size is the same as the starting size from the previous iteration, do not
// resize the main table, instead just use as is, otherwise it will oscillate.
if (nextWidth !== currentWidth) {
Copy link
Contributor Author

@Marc-Andre-Rivet Marc-Andre-Rivet Jun 16, 2020

Choose a reason for hiding this comment

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

Tested these for scenarios involving decimal places and is seems to behave fine on all important targets, decimals or not. There doesn't seem to be a need for an epsilon.

Marc-André Rivet added 2 commits June 16, 2020 16:28
style_header,
style_header_conditional,
style_table
);
Copy link
Contributor Author

@Marc-Andre-Rivet Marc-Andre-Rivet Jun 16, 2020

Choose a reason for hiding this comment

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

Only resize through React pipeline if a style prop has changed (memoized: https://github.com/plotly/dash-table/pull/793/files#diff-a6637ea69a08d602c311a574b4e4979aR324). This behaves similarly to the 🔪 forcedResizedOnly state prop, but also allows style changes to trigger recalculation. For example display:none;, changes in column widths, etc.


tr.style.height = getComputedStyle(tr2).height;
});
this.handleResize();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If executing this, the styles have changed, the above clears up all forced width before handling the resize, allowing for the table to resize itself correctly.

// Not pixels -> not displayed
if (!/\d+px/.test(currentTableWidth)) {
return;
}
Copy link
Contributor Author

@Marc-Andre-Rivet Marc-Andre-Rivet Jun 16, 2020

Choose a reason for hiding this comment

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

If not px, the table is not rendered, don't resize it. Lucky plotly/plotly.js#4925 happened at the same time, otherwise it would have gone unnoticed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OIC, '100.1px' will in fact pass this test as it's not a whole string match. Worried me at first but it should work.


def cells_are_same_width(target, table):
wait.until(lambda: abs(target.size["width"] - table.size["width"]) <= 1, 3)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Helper function that checks if two tables that are expected to have the same size / cell sizes are identical (or nearly identical) in size

Copy link
Collaborator

Choose a reason for hiding this comment

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

This routine is testing the last row of cells; while we're here would it be worthwhile checking that all the rows of a single table are the same, ie rows aren't ragged? That's certainly been a problem in the past...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Jagged rows happen when we transition from one fragment to another of the DataTable. Checking the cells of any row of a fragment against the cells of any row of the .cell-1-1 fragment should be sufficient if the table doesn't have a fixed portion.

What I realize from this comment is that for szng002 I should also be testing the target against itself.

fixed_columns=dict(headers=True, data=1),
fixed_rows=dict(headers=True, data=1),
),
]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create all combinations of fixed columns/rows tables and hide them.

style_table=dict(
width=750, minWidth=750, maxWidth=750, paddingBottom=10
)
),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tables are used to make sure the test tables are behaving as expected when updated through the callback below

return [
dict(width=350, minWidth=350, maxWidth=350, paddingBottom=10)
for i in variations_range
]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Change the table style to make them visible / hidden and/or resized

Copy link
Collaborator

Choose a reason for hiding this comment

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

Beautiful test :)
Looks like you started to collect all these style_table dicts up above in styles then changed tacks and put them inline down here... could probably be simplified a little, also we don't really need n clones, could just be return [dict(...)] * variations_range... but 🤷 not important for a test, the functionality is great!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test did change a lot between this version and the first implementation. Cleaned it up. 575aa2c


target = test.driver.find_element_by_css_selector("#table0")

for i in range(1, len(variations)):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test various configurations of fixed columns/rows and merge_duplicate_headers to make sure they all render the same with % based column widths.


ths.forEach(this.clearCellWidth);
});
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Force all th rows with all cells to match the expected width.

const tr = th.parentElement as HTMLElement;
this.resetFragmentCells(r0c0);
this.resetFragmentCells(r0c1);
this.resetFragmentCells(r1c0);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor - skip reset for a table that doesn't exist? Also seems like this could be 🌴

[r0c0, r0c1, r1c0].forEach(rc=> {
    rcTable=...;
    if (rcTable) { rcTable.style.width = ''; this.resetFragmentCells(rc); }
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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


variations_range = range(0, len(variations))

tables = [DataTable(**variation) for i, variation in enumerate(variations)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
tables = [DataTable(**variation) for i, variation in enumerate(variations)]
tables = [DataTable(**variation) for variation in variations]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"name": [
"A-----------------VERY LONG",
"B-----------------VERY LONG",
"C-----------------VERY LONG",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to have the last row of names all be different lengths, so we know it's not just the content match giving rise to the width match?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

{"_": 0, "a": 809, "b": 591, "c": 511},
],
style_table=dict(width=500, minWidth=500, maxWidth=500, paddingBottom=10),
style_cell=dict(width="20%", minWidth="20%", maxWidth="20%"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I only see 4 columns - is it intentional to have <100% total width?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Marc-Andre-Rivet
Copy link
Contributor Author

@alexcjohnson Addressed all previous comments. Also added b02a022 to fix #780 and lock behavior with new tests.

Copy link
Collaborator

@alexcjohnson alexcjohnson left a comment

Choose a reason for hiding this comment

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

Fantastic!💃

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
2 participants