-
Notifications
You must be signed in to change notification settings - Fork 68
[BUGFIX] Table: align filter rows with table columns #755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,7 @@ | |
| import { CellSettings, ColumnSettings, evaluateConditionalFormatting, TableOptions } from '../models'; | ||
| import { buildRawTableData, getTablePanelQueryMode } from '../table-data-utils'; | ||
| import { EmbeddedPanel } from './EmbeddedPanel'; | ||
| import { createPortal } from 'react-dom'; | ||
|
|
||
| type FilterValuesType<T> = Array<{ original: T; formatted: T }>; | ||
|
|
||
|
|
@@ -401,6 +402,10 @@ | |
| }); | ||
|
|
||
| const filteredDataRef = useRef<Array<Record<string, unknown>>>([]); | ||
| // Refs used to keep the filter row in sync with the table's horizontal | ||
| const panelContainerRef = useRef<HTMLDivElement>(null); | ||
| const filterRowInnerRef = useRef<HTMLDivElement>(null); | ||
| const filterCellRefs = useRef<Array<HTMLDivElement | null>>([]); | ||
|
|
||
| // Convert selectionMap to TanStack's RowSelectionState format | ||
| const rowSelection = useMemo((): RowSelectionState => { | ||
|
|
@@ -728,6 +733,77 @@ | |
| } | ||
| }, [spec.pagination, pagination]); | ||
|
|
||
| // Sync the filter row's horizontal position with the table scroll. | ||
| useEffect(() => { | ||
| if (!spec.enableFiltering) { | ||
| return; | ||
| } | ||
|
|
||
| const scrollContainer = panelContainerRef.current?.querySelector<HTMLElement>('.MuiTableContainer-root'); | ||
| const filterRowInner = filterRowInnerRef.current; | ||
|
|
||
| if (!scrollContainer || !filterRowInner) { | ||
| return; | ||
| } | ||
|
|
||
| const syncFilterRowScroll = (): void => { | ||
| filterRowInner.style.transform = `translateX(-${scrollContainer.scrollLeft}px)`; | ||
|
|
||
| setOpenFilterColumn((current) => (current === null ? current : null)); | ||
| setFilterAnchorEl({}); | ||
| }; | ||
|
|
||
| syncFilterRowScroll(); | ||
|
|
||
| scrollContainer.addEventListener('scroll', syncFilterRowScroll, { passive: true }); | ||
| return (): void => { | ||
| scrollContainer.removeEventListener('scroll', syncFilterRowScroll); | ||
| }; | ||
| }, [spec.enableFiltering, columns, contentDimensions]); | ||
|
|
||
| // Sync filter cell widths with the actual rendered table column widths to keep them aligned. | ||
| useEffect(() => { | ||
| if (!spec.enableFiltering) { | ||
| return; | ||
| } | ||
|
|
||
| const scrollContainer = panelContainerRef.current?.querySelector<HTMLElement>('.MuiTableContainer-root'); | ||
| if (!scrollContainer) { | ||
| return; | ||
| } | ||
|
|
||
| const syncColumnWidths = (): void => { | ||
| const headerCells = scrollContainer.querySelectorAll<HTMLElement>('thead tr th'); | ||
|
|
||
| columns.forEach((_, idx) => { | ||
| const headerCell = headerCells[idx]; | ||
| const filterCell = filterCellRefs.current[idx]; | ||
| if (!headerCell || !filterCell) { | ||
| return; | ||
| } | ||
|
|
||
| const width = `${headerCell.getBoundingClientRect().width}px`; | ||
| filterCell.style.width = width; | ||
| filterCell.style.minWidth = width; | ||
| filterCell.style.maxWidth = width; | ||
| }); | ||
| }; | ||
|
|
||
| syncColumnWidths(); | ||
|
|
||
| // Re-sync whenever the header row's size changes | ||
| const headerRow = scrollContainer.querySelector('thead tr'); | ||
|
Contributor
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. You could've lifted this line up next to the const scrollContainer = panelContainerRef.current?.querySelector<HTMLElement>('.MuiTableContainer-root');
const headerRow = scrollContainer?.querySelector('thead tr');
if(!headerRow){
return;
}With an early return you could've avoided the execution of some lines and also unnecessary heap memory consumption. WDYT? But before taking any action, does it make sense to initially call
Author
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. You're right, it doesn't make sense to run syncColumnWidths() without a header row. Rearranged to return early |
||
| const resizeObserver = new ResizeObserver(syncColumnWidths); | ||
| if (headerRow) { | ||
| resizeObserver.observe(headerRow); | ||
| } | ||
|
|
||
| return (): void => { | ||
| resizeObserver.disconnect(); | ||
| }; | ||
| }, [spec.enableFiltering, columns, contentDimensions, selectionEnabled, actionButtons]); | ||
|
|
||
|
|
||
| if (contentDimensions === undefined) { | ||
| return null; | ||
| } | ||
|
|
@@ -748,99 +824,119 @@ | |
| } | ||
|
|
||
| return ( | ||
| <> | ||
| <div ref={panelContainerRef} style={{ display: 'contents' }}> | ||
| {confirmDialog} | ||
| {spec.enableFiltering && ( | ||
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| overflow: 'hidden', | ||
| background: theme.palette.background.default, | ||
| borderBottom: `1px solid ${theme.palette.divider}`, | ||
| width: contentDimensions.width, | ||
| boxSizing: 'border-box', | ||
| }} | ||
| > | ||
| {columns.map((column, idx) => { | ||
| const filters = getSelectedFilterValues(column.accessorKey as string); | ||
| const columnWidth = column.width || spec.defaultColumnWidth; | ||
| return ( | ||
| <div | ||
| key={`filter-${idx}`} | ||
| style={{ | ||
| padding: '8px', | ||
| borderRight: idx < columns.length - 1 ? `1px solid ${theme.palette.divider}` : 'none', | ||
| width: columnWidth, | ||
| minWidth: columnWidth, | ||
| maxWidth: columnWidth, | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| position: 'relative', | ||
| boxSizing: 'border-box', | ||
| flex: typeof columnWidth === 'number' ? 'none' : '1 1 auto', | ||
| }} | ||
| > | ||
| <span | ||
| style={{ | ||
| marginRight: 8, | ||
| fontSize: '12px', | ||
| color: theme.palette.text.secondary, | ||
| flex: 1, | ||
| overflow: 'hidden', | ||
| textOverflow: 'ellipsis', | ||
| whiteSpace: 'nowrap', | ||
| }} | ||
| > | ||
| {filters.length ? `${filters.length} items` : 'All'} | ||
| </span> | ||
| <button | ||
| onClick={(e) => { | ||
| handleFilterClick(e, column.accessorKey as string); | ||
|
|
||
| <div | ||
| ref={filterRowInnerRef} | ||
| style={{ | ||
| display: 'flex', | ||
| width: 'max-content', | ||
| willChange: 'transform', | ||
| }} | ||
| > | ||
| {columns.map((column, idx) => { | ||
| const filters = getSelectedFilterValues(column.accessorKey as string); | ||
| const columnWidth = column.width || spec.defaultColumnWidth; | ||
| const anchorEl = filterAnchorEl[column.accessorKey as string]; | ||
|
|
||
| return ( | ||
| <div | ||
| key={`filter-${idx}`} | ||
|
Comment on lines
+848
to
+855
Contributor
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. I know this is the legacy part. But let's see if we can improve it. |
||
| ref={(el) => { | ||
| filterCellRefs.current[idx] = el; | ||
| }} | ||
| style={{ | ||
| border: `1px solid ${theme.palette.divider}`, | ||
| background: theme.palette.background.paper, | ||
| cursor: 'pointer', | ||
| fontSize: '12px', | ||
| color: filters.length ? theme.palette.primary.main : theme.palette.text.secondary, | ||
| padding: '4px 8px', | ||
| borderRadius: '4px', | ||
| minWidth: '20px', | ||
| height: '24px', | ||
| flexShrink: 0, | ||
| transition: 'all 0.2s ease', | ||
| }} | ||
| onMouseEnter={(e) => { | ||
| e.currentTarget.style.background = theme.palette.action.hover; | ||
| }} | ||
| onMouseLeave={(e) => { | ||
| e.currentTarget.style.background = theme.palette.background.paper; | ||
| padding: '8px', | ||
| borderRight: idx < columns.length - 1 ? `1px solid ${theme.palette.divider}` : 'none', | ||
| // These are just a starting size for the first paint — | ||
| // the width-sync effect above overwrites them with the | ||
| // table's actual rendered column widths. | ||
| width: columnWidth, | ||
| minWidth: columnWidth, | ||
| maxWidth: columnWidth, | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| position: 'relative', | ||
| boxSizing: 'border-box', | ||
| flex: 'none', | ||
| }} | ||
| type="button" | ||
| > | ||
| ▼ | ||
| </button> | ||
|
|
||
| {openFilterColumn === column.accessorKey && ( | ||
| <div | ||
| <span | ||
| style={{ | ||
| marginRight: 8, | ||
| fontSize: '12px', | ||
| color: theme.palette.text.secondary, | ||
| flex: 1, | ||
| overflow: 'hidden', | ||
| textOverflow: 'ellipsis', | ||
| whiteSpace: 'nowrap', | ||
| }} | ||
| > | ||
| {filters.length ? `${filters.length} items` : 'All'} | ||
| </span> | ||
| <button | ||
| onClick={(e) => { | ||
| handleFilterClick(e, column.accessorKey as string); | ||
| }} | ||
| style={{ | ||
| position: 'absolute', | ||
| top: '100%', | ||
| left: 0, | ||
| zIndex: 1000, | ||
| marginTop: 4, | ||
| border: `1px solid ${theme.palette.divider}`, | ||
| background: theme.palette.background.paper, | ||
| cursor: 'pointer', | ||
| fontSize: '12px', | ||
| color: filters.length ? theme.palette.primary.main : theme.palette.text.secondary, | ||
| padding: '4px 8px', | ||
| borderRadius: '4px', | ||
| minWidth: '20px', | ||
| height: '24px', | ||
| flexShrink: 0, | ||
| transition: 'all 0.2s ease', | ||
| }} | ||
| onMouseEnter={(e) => { | ||
| e.currentTarget.style.background = theme.palette.action.hover; | ||
| }} | ||
| onMouseLeave={(e) => { | ||
| e.currentTarget.style.background = theme.palette.background.paper; | ||
| }} | ||
| type="button" | ||
| > | ||
| <ColumnFilterDropdown | ||
| allValues={columnUniqueValues[column.accessorKey as string] || []} | ||
| selectedValues={filters} | ||
| onFilterChange={(values) => updateColumnFilter(column.accessorKey as string, values)} | ||
| theme={theme} | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| })} | ||
| ▼ | ||
| </button> | ||
|
|
||
| {openFilterColumn === column.accessorKey && | ||
| anchorEl && | ||
| createPortal( | ||
| <div | ||
| style={{ | ||
| position: 'fixed', | ||
| top: anchorEl.getBoundingClientRect().bottom + 4, | ||
| left: anchorEl.getBoundingClientRect().left, | ||
| zIndex: 1300, | ||
|
Contributor
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. Where does this
Author
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. no good reason to hard-code it. It happens to match MUI's own theme.zIndex.modal so I've referenced that directly instead of the magic number. Will update it to zIndex: theme.zIndex.modal, |
||
| }} | ||
| > | ||
| <ColumnFilterDropdown | ||
| allValues={columnUniqueValues[column.accessorKey as string] || []} | ||
| selectedValues={filters} | ||
| onFilterChange={(values) => updateColumnFilter(column.accessorKey as string, values)} | ||
| theme={theme} | ||
| /> | ||
| </div>, | ||
| document.body | ||
| )} | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| )} | ||
| <Table | ||
|
|
@@ -862,6 +958,6 @@ | |
| getItemActions={({ id, data }) => getItemActionButtons({ id, data: data as Record<string, unknown> })} | ||
| hasItemActions={actionButtons && actionButtons.length > 0} | ||
| /> | ||
| </> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
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.
It seems the set value is
nullanyway.Your logic => if current is
nullset it to current, otherwise set it tonull.So, couldn't you simply do something like this? Am I missing something?
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.
You are right
Simplified to a plain