Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 72
Issue 777 - Align column headers when using fixed rows #793
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
54d7af0e4f67557fde333d041141dd781474294d5bff92e93cb09457d773e4f95d9f999eda74e2bf938a09208c7f7bfefbbf31a03cffa5eb47a3573fc95bc72c11c5cdf9be457fd0a616f3a78d4ffbb86322e3b7dfbb4db482ae55d91466bfb85a33f55dc8d350703a4f5047e575aa2c4dcc753b02a022File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -39,6 +39,8 @@ import reconcile from 'dash-table/type/reconcile'; | ||
| import PageNavigation from 'dash-table/components/PageNavigation'; | ||
| type Refs = { [key: string]: HTMLElement }; | ||
| const DEFAULT_STYLE = { | ||
| width: '100%' | ||
| }; | ||
| @@ -48,9 +50,6 @@ const INNER_STYLE = { | ||
| minWidth: '100%' | ||
| }; | ||
| const WIDTH_EPSILON = 0.5; | ||
| const MAX_WIDTH_ITERATIONS = 30; | ||
| export default class ControlledTable extends PureComponent<ControlledTableProps> { | ||
| private readonly menuRef = React.createRef<HTMLDivElement>(); | ||
| private readonly stylesheet: Stylesheet = new Stylesheet(`#${CSS.escape(this.props.id)}`); | ||
| @@ -96,7 +95,7 @@ export default class ControlledTable extends PureComponent<ControlledTableProps> | ||
| return; | ||
| } | ||
| const { r1c1 } = this.refs as { [key: string]: HTMLElement }; | ||
| const { r1c1 } = this.refs as Refs; | ||
| let parent: any = r1c1.parentElement; | ||
| if (uiViewport && | ||
| @@ -150,7 +149,33 @@ export default class ControlledTable extends PureComponent<ControlledTableProps> | ||
| componentDidUpdate() { | ||
| this.updateStylesheet(); | ||
| this.updateUiViewport(); | ||
| this.handleResize(); | ||
| const { | ||
| style_as_list_view, | ||
| style_cell, | ||
| style_cell_conditional, | ||
| style_data, | ||
| style_data_conditional, | ||
| style_filter, | ||
| style_filter_conditional, | ||
| style_header, | ||
| style_header_conditional, | ||
| style_table | ||
| } = this.props; | ||
| this.handleResizeIf( | ||
| style_as_list_view, | ||
| style_cell, | ||
| style_cell_conditional, | ||
| style_data, | ||
| style_data_conditional, | ||
| style_filter, | ||
| style_filter_conditional, | ||
| style_header, | ||
| style_header_conditional, | ||
| style_table | ||
| ); | ||
| ||
| this.handleDropdown(); | ||
| this.adjustTooltipPosition(); | ||
| @@ -168,7 +193,7 @@ export default class ControlledTable extends PureComponent<ControlledTableProps> | ||
| return; | ||
| } | ||
| const { r1c1 } = this.refs as { [key: string]: HTMLElement }; | ||
| const { r1c1 } = this.refs as Refs; | ||
| const contentTd = r1c1.querySelector('tr > td:first-of-type'); | ||
| if (!contentTd) { | ||
| @@ -225,119 +250,184 @@ export default class ControlledTable extends PureComponent<ControlledTableProps> | ||
| } | ||
| } | ||
| forceHandleResize = () => this.handleResize(true); | ||
| private clearCellWidth(cell: HTMLElement) { | ||
| cell.style.width = ''; | ||
| cell.style.minWidth = ''; | ||
| cell.style.maxWidth = ''; | ||
| cell.style.boxSizing = ''; | ||
| } | ||
| handleResize = (force: boolean = false) => { | ||
| const { | ||
| fixed_columns, | ||
| fixed_rows, | ||
| forcedResizeOnly, | ||
| setState | ||
| } = this.props; | ||
| private resetFragmentCells = ( | ||
| fragment: HTMLElement | ||
| ) => { | ||
| const lastRowOfCells = fragment.querySelectorAll<HTMLElement>('table.cell-table > tbody > tr:last-of-type > *'); | ||
| if (!lastRowOfCells.length) { | ||
| return; | ||
| } | ||
| if (forcedResizeOnly && !force) { | ||
| Array.from( | ||
| lastRowOfCells | ||
| ).forEach(this.clearCellWidth); | ||
| const firstThs = Array.from(fragment.querySelectorAll('table.cell-table > tbody > tr > th:first-of-type')); | ||
| const trOfThs = firstThs.map(th => th.parentElement); | ||
| trOfThs.forEach(tr => { | ||
| const ths = Array.from<HTMLElement>(tr?.children as any); | ||
| if (!ths) { | ||
| return; | ||
| } | ||
| ths.forEach(this.clearCellWidth); | ||
| }); | ||
| } | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Force all | ||
| resizeFragmentCells = ( | ||
| fragment: HTMLElement, | ||
| widths: number[] | ||
| ) => { | ||
| const lastRowOfCells = fragment.querySelectorAll<HTMLElement>('table.cell-table > tbody > tr:last-of-type > *'); | ||
| if (!lastRowOfCells.length) { | ||
| return; | ||
| } | ||
| if (!force) { | ||
| setState({ forcedResizeOnly: true }); | ||
| Array.from( | ||
| lastRowOfCells | ||
| ).forEach((c, i) => this.setCellWidth(c, widths[i])); | ||
| const firstThs = Array.from<HTMLElement>(fragment.querySelectorAll('table.cell-table > tbody > tr > th:first-of-type')); | ||
| const trOfThs = firstThs.map(th => th.parentElement); | ||
| trOfThs.forEach(tr => { | ||
| const ths = Array.from<HTMLElement>(tr?.children as any); | ||
| if (!ths) { | ||
| return; | ||
| } | ||
| if (ths.length === widths.length) { | ||
| ths.forEach((c, i) => this.setCellWidth(c, widths[i])); | ||
| } else { | ||
| ths.forEach(c => this.setCellWidth(c, 0)); | ||
alexcjohnson marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| }); | ||
| } | ||
| resizeFragmentTable = (table: HTMLElement | null, width: string) => { | ||
| if (!table) { | ||
| return; | ||
| } | ||
| this.updateStylesheet(); | ||
| table.style.width = width; | ||
| } | ||
| getScrollbarWidth().then((scrollbarWidth: number) => setState({ scrollbarWidth })); | ||
| isDisplayed = (el: HTMLElement) => getComputedStyle(el).display !== 'none'; | ||
| const { r0c0, r0c1, r1c0, r1c1 } = this.refs as { [key: string]: HTMLElement }; | ||
| forceHandleResize = () => this.handleResize(); | ||
| handleResizeIf = memoizeOne((..._: any[]) => { | ||
| const { r0c0, r0c1, r1c0, r1c1 } = this.refs as Refs; | ||
| // Adjust [fixed columns/fixed rows combo] to fixed rows height | ||
| let trs = r0c1.querySelectorAll('tr'); | ||
| Array.from(r0c0.querySelectorAll('tr')).forEach((tr, index) => { | ||
| const tr2 = trs[index]; | ||
| if (!this.isDisplayed(r1c1)) { | ||
| return; | ||
| } | ||
| tr.style.height = `${tr2.clientHeight}px`; | ||
| }); | ||
| r0c1.style.marginLeft = ''; | ||
| r1c1.style.marginLeft = ''; | ||
| r0c0.style.width = ''; | ||
| r1c0.style.width = ''; | ||
| // Adjust fixed columns headers to header's height | ||
| let trths = r1c1.querySelectorAll('tr > th:first-of-type'); | ||
| Array.from(r1c0.querySelectorAll('tr > th:first-of-type')).forEach((th, index) => { | ||
| const tr2 = trths[index].parentElement as HTMLElement; | ||
| const tr = th.parentElement as HTMLElement; | ||
| [r0c0, r0c1, r1c0].forEach(rc => { | ||
| const table = rc.querySelector('table'); | ||
| if (table) { | ||
| table.style.width = ''; | ||
| } | ||
| tr.style.height = getComputedStyle(tr2).height; | ||
| this.resetFragmentCells(rc); | ||
| }); | ||
| if (fixed_columns) { | ||
| const r1c0Table = r1c0.querySelector('table') as HTMLElement; | ||
| const r1c1Table = r1c0.querySelector('table') as HTMLElement; | ||
| this.handleResize(); | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| }); | ||
| r1c0Table.style.width = getComputedStyle(r1c1Table).width; | ||
| handleResize = (previousWidth: number = NaN, cycle: boolean = false) => { | ||
| const { | ||
| fixed_columns, | ||
| fixed_rows, | ||
| setState | ||
| } = this.props; | ||
| const lastVisibleTd = r1c0.querySelector(`tr:first-of-type > *:nth-of-type(${fixed_columns})`); | ||
| const { r1c1 } = this.refs as Refs; | ||
| let it = 0; | ||
| let currentWidth = r1c0.getBoundingClientRect().width; | ||
| let lastWidth = currentWidth; | ||
| if (!this.isDisplayed(r1c1)) { | ||
| return; | ||
| } | ||
| ||
| do { | ||
| lastWidth = currentWidth | ||
| this.updateStylesheet(); | ||
| // Force first column containers width to match visible portion of table | ||
| if (lastVisibleTd) { | ||
| const r1c0FragmentBounds = r1c0.getBoundingClientRect(); | ||
| const lastTdBounds = lastVisibleTd.getBoundingClientRect(); | ||
| currentWidth = lastTdBounds.right - r1c0FragmentBounds.left; | ||
| getScrollbarWidth().then((scrollbarWidth: number) => setState({ scrollbarWidth })); | ||
| const width = `${currentWidth}px`; | ||
| const { r0c0, r0c1, r1c0 } = this.refs as Refs; | ||
| r0c0.style.width = width; | ||
| r1c0.style.width = width; | ||
| } | ||
| const r0c0Table = r0c0.querySelector('table'); | ||
| const r0c1Table = r0c1.querySelector('table'); | ||
| const r1c0Table = r1c0.querySelector('table'); | ||
| const r1c1Table = r1c1.querySelector('table') as HTMLElement; | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Casting - this is the only fragment that is always present | ||
| // Force second column containers width to match visible portion of table | ||
| const firstVisibleTd = r1c1.querySelector(`tr:first-of-type > *:nth-of-type(${fixed_columns + 1})`); | ||
| if (firstVisibleTd) { | ||
| const r1c1FragmentBounds = r1c1.getBoundingClientRect(); | ||
| const firstTdBounds = firstVisibleTd.getBoundingClientRect(); | ||
| const width = firstTdBounds.left - r1c1FragmentBounds.left; | ||
| r0c1.style.marginLeft = `-${width}px`; | ||
| r0c1.style.marginRight = `${width}px`; | ||
| r1c1.style.marginLeft = `-${width}px`; | ||
| r1c1.style.marginRight = `${width}px`; | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
| ||
| } | ||
| const currentTableWidth = getComputedStyle(r1c1Table).width; | ||
| it++; | ||
| } while ( | ||
| Math.abs(currentWidth - lastWidth) > WIDTH_EPSILON || | ||
| it < MAX_WIDTH_ITERATIONS | ||
| ) | ||
| if (!cycle) { | ||
| this.resizeFragmentTable(r0c0Table, currentTableWidth); | ||
| this.resizeFragmentTable(r0c1Table, currentTableWidth); | ||
| this.resizeFragmentTable(r1c0Table, currentTableWidth); | ||
| } | ||
| ||
| if (fixed_columns || fixed_rows) { | ||
| const r1c0CellWidths = Array.from( | ||
| r1c0.querySelectorAll('table.cell-table > tbody > tr:first-of-type > *') | ||
| ).map(c => c.getBoundingClientRect().width); | ||
| const r1c1CellWidths = Array.from( | ||
| const widths = Array.from( | ||
| r1c1.querySelectorAll('table.cell-table > tbody > tr:first-of-type > *') | ||
| ).map(c => c.getBoundingClientRect().width); | ||
| Array.from<HTMLElement>( | ||
| r0c0.querySelectorAll('table.cell-table > tbody > tr:first-of-type > *') | ||
| ).forEach((c, i) => this.setCellWidth(c, r1c0CellWidths[i])); | ||
| if (!cycle) { | ||
| this.resizeFragmentCells(r0c0, widths); | ||
| this.resizeFragmentCells(r0c1, widths); | ||
| this.resizeFragmentCells(r1c0, widths); | ||
| } | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If not in a cycle, try and force all columns to be the same size as the columns from the main fragment. If in a cycle, don't do anything - the last calculated values will be used. | ||
| } | ||
| if (fixed_columns) { | ||
| const lastFixedTd = r1c1.querySelector(`tr:first-of-type > *:nth-of-type(${fixed_columns})`); | ||
| if (lastFixedTd) { | ||
| const lastFixedTdBounds = lastFixedTd.getBoundingClientRect(); | ||
| const lastFixedTdRight = lastFixedTdBounds.right - r1c1.getBoundingClientRect().left; | ||
| Array.from<HTMLElement>( | ||
| r0c0.querySelectorAll('table.cell-table > tbody > tr:last-of-type > *') | ||
| ).forEach((c, i) => this.setCellWidth(c, r1c0CellWidths[i])); | ||
| // Force first column containers width to match visible portion of table | ||
| r0c0.style.width = `${lastFixedTdRight}px`; | ||
| r1c0.style.width = `${lastFixedTdRight}px`; | ||
| ||
| if (!cycle) { | ||
| // Force second column containers width to match visible portion of table | ||
| const firstVisibleTd = r1c1.querySelector(`tr:first-of-type > *:nth-of-type(${fixed_columns + 1})`); | ||
| if (firstVisibleTd) { | ||
| const r1c1FragmentBounds = r1c1.getBoundingClientRect(); | ||
| const firstTdBounds = firstVisibleTd.getBoundingClientRect(); | ||
| const width = firstTdBounds.left - r1c1FragmentBounds.left; | ||
| r0c1.style.marginLeft = `-${width}px`; | ||
| r1c1.style.marginLeft = `-${width}px`; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Array.from<HTMLElement>( | ||
| r0c1.querySelectorAll('table.cell-table > tbody > tr:first-of-type > *') | ||
| ).forEach((c, i) => this.setCellWidth(c, r1c1CellWidths[i])); | ||
| if (!cycle) { | ||
| const currentWidth = parseInt(currentTableWidth, 10); | ||
| const nextWidth = parseInt(getComputedStyle(r1c1Table).width, 10); | ||
| Array.from<HTMLElement>( | ||
| r0c1.querySelectorAll('table.cell-table > tbody > tr:last-of-type > *') | ||
| ).forEach((c, i) => this.setCellWidth(c, r1c1CellWidths[i])); | ||
| // 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) { | ||
| ||
| this.handleResize(currentWidth, nextWidth === previousWidth); | ||
| } | ||
| } | ||
| } | ||
| @@ -699,17 +789,17 @@ export default class ControlledTable extends PureComponent<ControlledTableProps> | ||
| } | ||
| handleDropdown = () => { | ||
| const { r1c1 } = this.refs as { [key: string]: HTMLElement }; | ||
| const { r1c1 } = this.refs as Refs; | ||
| dropdownHelper(r1c1.querySelector('.Select-menu-outer')); | ||
| } | ||
| onScroll = (ev: any) => { | ||
| const { r0c0, r0c1 } = this.refs as { [key: string]: HTMLElement }; | ||
| const { r0c0, r0c1 } = this.refs as Refs; | ||
| Logger.trace(`ControlledTable fragment scrolled to (left,top)=(${ev.target.scrollLeft},${ev.target.scrollTop})`); | ||
| const margin = parseFloat(ev.target.scrollLeft) + parseFloat(r0c0.style.width); | ||
| const margin = parseFloat(ev.target.scrollLeft) + (parseFloat(r0c0.style.width) || 0); | ||
| r0c1.style.marginLeft = `${-margin}px`; | ||
| @@ -945,23 +1035,15 @@ export default class ControlledTable extends PureComponent<ControlledTableProps> | ||
| } | ||
| } | ||
| private setCellWidth(cell: HTMLElement, width: number) { | ||
| cell.style.width = `${width}px`; | ||
| cell.style.minWidth = `${width}px`; | ||
| cell.style.maxWidth = `${width}px`; | ||
| cell.style.boxSizing = 'border-box'; | ||
| /** | ||
| * Some browsers handle `th` and `td` size inconsistently. | ||
| * Checking the size delta and adjusting for it (different handling of padding and borders) | ||
| * allows the table to make sure all sections are correctly aligned. | ||
| */ | ||
| const delta = cell.getBoundingClientRect().width - width; | ||
| if (delta) { | ||
| cell.style.width = `${width - delta}px`; | ||
| cell.style.minWidth = `${width - delta}px`; | ||
| cell.style.maxWidth = `${width - delta}px`; | ||
| private setCellWidth(cell: HTMLElement, width: string | number) { | ||
| if (typeof width === 'number') { | ||
| width = `${width}px`; | ||
| } | ||
| cell.style.width = width; | ||
| cell.style.minWidth = width; | ||
| cell.style.maxWidth = width; | ||
| cell.style.boxSizing = 'border-box'; | ||
| } | ||
| private get showToggleColumns(): boolean { | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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,
localhostis redirected tobs-localwhich webpack-dev-server doesn't like, this flag, as its name indicates, makes it not care