-
Notifications
You must be signed in to change notification settings - Fork 6
Sticky header using intersection observer for recordset page #2608
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: master
Are you sure you want to change the base?
Changes from all commits
e4366eb
35cfd8b
1061ab1
314c1d9
a6b4083
084ab9d
b5f6adc
2fb2a86
e7a17f3
f6e1cf6
d51e1f5
ae4f216
4459ba9
60bb669
60a366d
08c8c06
ca340f9
a5fba7c
f2dfff2
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 |
---|---|---|
|
@@ -33,13 +33,16 @@ type RecordsetTableProps = { | |
* Determines if both horizontal scrollbars should always be visible, or if only one should appear at a time. | ||
*/ | ||
showSingleScrollbar?: boolean, | ||
// Determines the top for the sticky header | ||
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. Use JSDoc style of comment like the previous prop that you see. So
|
||
headerTop?: number, | ||
sortCallback?: (sortColumn: SortColumn) => any | ||
} | ||
|
||
const RecordsetTable = ({ | ||
config, | ||
initialSortObject, | ||
showSingleScrollbar = false | ||
showSingleScrollbar = false, | ||
headerTop, | ||
}: RecordsetTableProps): JSX.Element => { | ||
|
||
const { | ||
|
@@ -60,6 +63,10 @@ const RecordsetTable = ({ | |
const tableContainer = useRef<HTMLDivElement>(null); | ||
const stickyScrollbarRef = useRef<HTMLDivElement>(null); | ||
const tableEndRef = useRef<HTMLDivElement>(null); | ||
const tableRef = useRef<HTMLTableElement>(null); | ||
const outerTableRef = useRef<HTMLDivElement>(null); | ||
const headRef = useRef<HTMLTableSectionElement>(null); | ||
const stickyHeaderRef = useRef<HTMLDivElement>(null); | ||
|
||
|
||
const [currSortColumn, setCurrSortColumn] = useState<SortColumn | null>( | ||
|
@@ -157,10 +164,75 @@ const RecordsetTable = ({ | |
} | ||
|
||
return () => { | ||
observer.disconnect(); | ||
observer.disconnect(); | ||
} | ||
}, []); | ||
|
||
//To handle sticky header visibility based on element intersection | ||
useLayoutEffect(() => { | ||
if (!outerTableRef.current || !headRef.current || !stickyHeaderRef.current || !stickyScrollbarRef.current) { | ||
return; | ||
} | ||
|
||
// Create an IntersectionObserver to track when the table header is visible | ||
const observer = new IntersectionObserver( | ||
([entry]) => { | ||
if (stickyHeaderRef.current) { | ||
if (!entry.isIntersecting) { | ||
stickyHeaderRef.current.style.visibility = 'visible'; | ||
// Adjust the sticky header position based on the presence of a scrollbar height and top panel container's height | ||
const scrollbarHeight = stickyScrollbarRef.current?.offsetHeight || 0; | ||
stickyHeaderRef.current.style.top=`${headerTop ? headerTop + scrollbarHeight:0}px`; | ||
} else { | ||
stickyHeaderRef.current.style.visibility = 'hidden'; | ||
} | ||
} | ||
}, | ||
{ root: null, threshold: 0 } | ||
); | ||
observer.observe(headRef.current); | ||
|
||
// Sync widths of the columns | ||
const syncWidths = () => { | ||
if (stickyHeaderRef.current && tableRef.current) { | ||
|
||
const originalThs = tableRef.current.querySelectorAll('tbody > tr > td'); | ||
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. Why is this selecting |
||
const stickyThs = stickyHeaderRef.current?.querySelectorAll('th'); | ||
|
||
// Loop through columns and set widths | ||
stickyThs!.forEach((headerCol, index) => { | ||
const dataCol = originalThs[index]; | ||
if (dataCol instanceof HTMLElement) { | ||
const colWidth = dataCol.offsetWidth; // Get the actual width of the column | ||
headerCol.style.width = `${colWidth}px`; // Set width on sticky header | ||
} | ||
|
||
}); | ||
stickyHeaderRef.current.style.width = `${outerTableRef.current?.offsetWidth}px`; | ||
} | ||
}; | ||
|
||
// Function to synchronize the horizontal scroll position of the sticky header with the scrollbar | ||
const handleScroll = () => { | ||
if (stickyHeaderRef.current && stickyScrollbarRef.current) { | ||
stickyHeaderRef.current.scrollLeft = stickyScrollbarRef.current.scrollLeft; | ||
} | ||
}; | ||
|
||
stickyScrollbarRef.current?.addEventListener('scroll', handleScroll); | ||
|
||
// Sync column widths on resize | ||
const headerWidthResizeObserver = new ResizeObserver(syncWidths); | ||
headerWidthResizeObserver.observe(outerTableRef.current); | ||
|
||
// Cleanup function | ||
return () => { | ||
observer.disconnect(); | ||
stickyScrollbarRef.current?.removeEventListener('scroll', handleScroll); | ||
headerWidthResizeObserver.disconnect(); | ||
}; | ||
|
||
}, [isInitialized, headerTop]); | ||
|
||
/** | ||
* add the top horizontal scroll if needed | ||
|
@@ -423,6 +495,15 @@ const RecordsetTable = ({ | |
}) | ||
} | ||
|
||
const renderHeader = () => { | ||
return( | ||
<tr> | ||
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. Fix the indentation to match the rest of the file. |
||
{showActionButtons && renderActionsHeader()} | ||
{renderColumnHeaders()} | ||
</tr> | ||
); | ||
} | ||
|
||
const renderRows = () => { | ||
if (hasTimeoutError) { | ||
return ( | ||
|
@@ -519,13 +600,10 @@ const RecordsetTable = ({ | |
className='chaise-table-top-scroll-wrapper'> | ||
<div className='chaise-table-top-scroll'></div> | ||
</div> | ||
<div className={outerTableClassname()}> | ||
<table className='table chaise-table table-hover'> | ||
<thead className='table-heading'> | ||
<tr> | ||
{showActionButtons && renderActionsHeader()} | ||
{renderColumnHeaders()} | ||
</tr> | ||
<div className={outerTableClassname()} ref={outerTableRef}> | ||
<table className='table chaise-table table-hover' ref={tableRef}> | ||
<thead className='table-heading' ref={headRef}> | ||
{renderHeader()} | ||
</thead> | ||
<tbody> | ||
{renderRows()} | ||
|
@@ -534,7 +612,14 @@ const RecordsetTable = ({ | |
</div> | ||
{/* This div will be used as the target (end of table) for the intersection observer to hide the | ||
top scrollbar when the bottom one is visible */} | ||
<div className='dummy-table-end-div' ref={tableEndRef}/> | ||
<div className='dummy-table-end-div' ref={tableEndRef} /> | ||
{config.displayMode.indexOf(RecordsetDisplayMode.RELATED) !== 0 && <div className='sticky-header' id='sticky-header' ref={stickyHeaderRef}> | ||
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. Create a variable for 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. Remove the |
||
<table className='sticky-header-table'> | ||
<thead className='table-heading sticky'> | ||
{renderHeader()} | ||
</thead> | ||
</table> | ||
</div>} | ||
|
||
{!hasTimeoutError && numHiddenRecords > 0 && | ||
<div className='chaise-table-footer'> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -168,6 +168,8 @@ const RecordsetInner = ({ | |
|
||
const [savedQueryUpdated, setSavedQueryUpdated] = useState<boolean>(false); | ||
|
||
const [topPanelDimensions, setTopPanelDimensions] = useState<any>({}); | ||
|
||
const [permalinkTooltip, setPermalinkTooltip] = useState(MESSAGE_MAP.tooltip.permalink); | ||
|
||
const mainContainer = useRef<HTMLDivElement>(null); | ||
|
@@ -367,7 +369,8 @@ const RecordsetInner = ({ | |
} | ||
|
||
// handle the scrollable container | ||
const resizeSensors = attachContainerHeightSensors(parentContainer, parentStickyArea); | ||
const resizeSensors = attachContainerHeightSensors(parentContainer, parentStickyArea, undefined, setTopPanelDimensions); | ||
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. The more I think about it, the more I don't like this solution. Since the callback is called on each height or width change, React is rerendering recordset component and all its children on each height/width change. So this solution would never work on record page, and it can slow the page down on long recordset pages. As I mentioned before, we're just manipulating the styles attached to the elements, so there's no reason to involve React state in this. Instead of calling |
||
|
||
|
||
// log the right click event on the permalink button | ||
const permalink = document.getElementById('permalink'); | ||
|
@@ -811,6 +814,7 @@ const RecordsetInner = ({ | |
<RecordsetTable | ||
config={config} | ||
initialSortObject={initialReference.location.sortObject} | ||
headerTop={topPanelDimensions?.top} | ||
/> | ||
</div> | ||
{config.displayMode === RecordsetDisplayMode.FULLSCREEN && <Footer />} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ import Tooltip from 'bootstrap/js/dist/tooltip'; | |
* @param {Node=} parentContainer - the parent container. if undefined `body` will be used. | ||
* @param {Node=} parentContainerSticky - the sticky area of parent. if undefined `.app-header-container` will be used. | ||
* @param {boolean} useDocHeight - whether we should use the doc height even if parentContainer is passed. | ||
* @param {Callback Function} onContainerHeightChange - Callback allows external functions to receive height or any other dimension updates. | ||
* Call this function once the DOM elements are loaded to attach resize sensors that will fix the height of bottom-panel-container | ||
* If you don't pass any parentContainer, it will use the body | ||
* It will assume the following structure in the given parentContainer: | ||
|
@@ -22,7 +23,7 @@ import Tooltip from 'bootstrap/js/dist/tooltip'; | |
* | ||
* TODO offsetHeight is a rounded integer, should we use getBoundingClientRect().height in this function instead? | ||
*/ | ||
export function attachContainerHeightSensors(parentContainer?: any, parentContainerSticky?: any, useDocHeight?: boolean) { | ||
export function attachContainerHeightSensors(parentContainer?: any, parentContainerSticky?: any, useDocHeight?: boolean, onContainerHeightChange?: (dimensions: any) => void) { | ||
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. Don't use export type ContainerHeightSensorDimensions = {
/**
* explain what it is here
*/
top: number;
} Also this line is now too long (you should see it being highlighted as a warning on VSCode if you have eslint installed). So please break it into multiple lines. |
||
try { | ||
const appRootId = `#${ID_NAMES.APP_ROOT}`; | ||
|
||
|
@@ -53,6 +54,7 @@ export function attachContainerHeightSensors(parentContainer?: any, parentContai | |
appContent.style.overflowY = 'auto'; | ||
appContent.style.height = ((parentUsableHeight / windowRef.innerHeight) * 100) + 'vh'; | ||
container.style.height = 'unset'; | ||
appContent.classList.add('app-content-container-scrollable'); | ||
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. Add this to |
||
} | ||
|
||
let tm: any; | ||
|
@@ -79,12 +81,14 @@ export function attachContainerHeightSensors(parentContainer?: any, parentContai | |
} | ||
|
||
const containerHeight = ((parentUsableHeight - stickyHeight) / windowRef.innerHeight) * 100; | ||
|
||
if (containerHeight < 15) { | ||
resetHeight(); | ||
} else { | ||
//remove the styles that might have been added to appContent | ||
appContent.style.overflowY = 'unset'; | ||
appContent.style.height = 'unset'; | ||
appContent.classList.remove('app-content-container-scrollable'); | ||
|
||
// set the container's height | ||
container.style.height = containerHeight + 'vh'; | ||
|
@@ -94,6 +98,12 @@ export function attachContainerHeightSensors(parentContainer?: any, parentContai | |
resetHeight(); | ||
} | ||
} | ||
if (onContainerHeightChange) { | ||
//To notify height change | ||
onContainerHeightChange({ | ||
top: containerSticky.offsetHeight + parentContainerSticky.offsetHeight, | ||
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. Instead of recalculating this, is it possible to change the code above to first calculate the top. And then sets the container height using this? What you have is not working properly for popups. You can test this on |
||
}); | ||
} | ||
} | ||
|
||
|
||
|
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.
Please rename this to
chaise-table-sticky-header
to be more specific.