From e4366eb411e3c48e4458bb4bc43e09fcb590f785 Mon Sep 17 00:00:00 2001 From: Keny Shah Date: Wed, 11 Dec 2024 09:11:44 -0800 Subject: [PATCH] sticky-header-intersection-observer --- src/assets/scss/_recordset-table.scss | 57 +++++- src/components/recordset/recordset-table.tsx | 183 ++++++++++++++++++- 2 files changed, 236 insertions(+), 4 deletions(-) diff --git a/src/assets/scss/_recordset-table.scss b/src/assets/scss/_recordset-table.scss index 9276a2a76..2fb6d3344 100644 --- a/src/assets/scss/_recordset-table.scss +++ b/src/assets/scss/_recordset-table.scss @@ -5,7 +5,7 @@ overflow-y: hidden; } -.chaise-table.table { +.chaise-table.table, .sticky-header > table { border-top: 1px solid map.get(variables.$color-map, 'table-border'); border-bottom: 1px solid map.get(variables.$color-map, 'table-border'); margin: 0; @@ -262,3 +262,58 @@ display: none !important; position: absolute !important; } +.sticky-header { + position: fixed; + top: 0; + // z-index: 1000; + visibility:none; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + overflow-x: hidden; +} +.sticky-header > table > thead > tr > th{ + font-weight: 400; + border-top: 1px solid map.get(variables.$color-map, 'table-border'); + position: relative; + padding: 7px; + + + &.actions-header { + white-space: nowrap; + text-align: center; + font-size: variables.$h4-font-size; + padding: 8px; + } + + .table-column-displayname { + font-size: variables.$h4-font-size; + } + + // preserve space for the icon on the right + // 20 for the "icon space" + // 10 for the "right indentation" + padding-right: 30px; + .table-heading-icons { + position: absolute; + bottom: 7px; + right: 10px; + font-size: variables.$h4-font-size; + + .table-column-spinner { + position: absolute; + bottom: 2px; + right: -5px; + + .spinner-border { + // NOTE: we should not customize the width/height since it causes wobbling effect + color: map.get(variables.$color-map, 'table-header-spinner'); + border-width: .16em; + } + } + } +} +.sticky-header{ +.sticky-header-table{ + table-layout: fixed; + width: 100%; +} +} \ No newline at end of file diff --git a/src/components/recordset/recordset-table.tsx b/src/components/recordset/recordset-table.tsx index 9f5174904..d9d902faf 100644 --- a/src/components/recordset/recordset-table.tsx +++ b/src/components/recordset/recordset-table.tsx @@ -60,6 +60,10 @@ const RecordsetTable = ({ const tableContainer = useRef(null); const stickyScrollbarRef = useRef(null); const tableEndRef = useRef(null); + const tableRef = useRef(null); + const outerTableRef = useRef(null); + const headRef = useRef(null); + const stickyHeaderRef = useRef(null); const [currSortColumn, setCurrSortColumn] = useState( @@ -70,6 +74,7 @@ const RecordsetTable = ({ // tracks whether a paging action has successfully occurred for this table // used for related tables to fire an event when the content has loaded to scroll back to the top of the related table const [pagingSuccess, setPagingSuccess] = useState(false); + const [headerTop, setHeaderTop] = useState(0); type RowConfig = { isSelected: boolean; @@ -161,7 +166,167 @@ const RecordsetTable = ({ } }, []); + useEffect(()=>{ + console.log('calllled'); + setHeaderTop(headRef.current!.getBoundingClientRect().top); + },[isInitialized]); + console.log(headerTop, headRef.current?.getBoundingClientRect()); + + useEffect(()=>{ + if(!outerTableRef.current || !headRef.current || !stickyHeaderRef.current){ + return; + } + + const observer = new IntersectionObserver( + ([entry]) => { + if(stickyHeaderRef.current){ + if (!entry.isIntersecting) { + stickyHeaderRef.current.style.visibility = 'visible'; + const hasScrollbar = outerTableRef.current!.scrollWidth > outerTableRef.current!.clientWidth; + console.log('hasScrollbar ',hasScrollbar); + stickyHeaderRef.current.style.top = `${headerTop}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'); + const stickyThs = stickyHeaderRef.current?.querySelectorAll('th'); + + // Loop through columns and set widths + stickyThs!.forEach((headerCol, index) => { + const dataCol = originalThs[index]; + if (dataCol instanceof HTMLElement) { // Ensure it's an HTML element + 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`; + } + }; + const handleScroll = () => { + if (stickyHeaderRef.current && stickyScrollbarRef.current) { + stickyHeaderRef.current.scrollLeft = stickyScrollbarRef.current.scrollLeft; + } + }; + + stickyScrollbarRef.current?.addEventListener('scroll', handleScroll); + + // Sync column widths on resize + window.addEventListener('resize', syncWidths); + + // Perform initial sync + syncWidths(); + + // Cleanup function + return () => { + observer.disconnect(); + stickyScrollbarRef.current?.removeEventListener('scroll', handleScroll); + window.removeEventListener('resize', syncWidths); + }; + + },[isLoading, headerTop]); + + // useEffect(() => { + // if (!outerTableRef.current || !headRef.current || !stickyHeaderRef.current) return; + + // // Intersection Observer for bottom scrollbar visibility + // const bottomObserver = new IntersectionObserver( + // ([entry]) => { + // if (stickyScrollbarRef.current) { + // if (entry.isIntersecting) { + // stickyScrollbarRef.current.classList.add('no-scroll-bar'); + // if(stickyHeaderRef.current){ + // stickyHeaderRef.current.style.visibility = 'hidden'; + // } + // } else { + // stickyScrollbarRef.current.classList.remove('no-scroll-bar'); + // } + // } + // }, + // { root: null, threshold: 0.1 } + // ); + + // // Intersection Observer for sticky header visibility + // const headerObserver = new IntersectionObserver( + // ([entry]) => { + // if (stickyHeaderRef.current) { + // if (!entry.isIntersecting) { + // stickyHeaderRef.current.style.visibility = 'visible'; + // stickyHeaderRef.current.style.top = `calc(${stickyScrollbarRef.current!.getBoundingClientRect().top}px + 15px)`; + // } else { + // stickyHeaderRef.current.style.visibility = 'hidden'; + // } + // } + // }, + // { root: null, threshold: 0 } + // ); + + // // Observe elements + // if (tableEndRef.current) bottomObserver.observe(tableEndRef.current); + // headerObserver.observe(headRef.current); + + // // Sync widths of the columns + // const syncWidths = () => { + // if (!stickyHeaderRef.current || !tableRef.current) return; + + // const originalThs = tableRef.current.querySelectorAll('tbody > tr > td'); + // const stickyThs = stickyHeaderRef.current?.querySelectorAll('th'); + + // stickyThs!.forEach((headerCol, index) => { + // const dataCol = originalThs[index]; + // if (dataCol instanceof HTMLElement) { + // const colWidth = dataCol.offsetWidth; + // headerCol.style.width = `${colWidth}px`; + // } + // }); + + // stickyHeaderRef.current.style.width = `${outerTableRef.current?.offsetWidth}px`; + + // console.log('headRef.current:', headRef.current?.offsetWidth, + // 'outerTableRef.current:', outerTableRef.current?.offsetWidth, + // 'stickyHeaderRef.current:', stickyHeaderRef.current.offsetWidth); + // }; + + // // Scroll synchronization + // const handleScroll = () => { + // if (stickyHeaderRef.current && stickyScrollbarRef.current) { + // stickyHeaderRef.current.scrollLeft = stickyScrollbarRef.current.scrollLeft; + // } + // }; + + // stickyScrollbarRef.current?.addEventListener('scroll', handleScroll); + + // // Sync column widths on resize + // window.addEventListener('resize', syncWidths); + + // // Perform initial sync + // syncWidths(); + + // // Cleanup function + // return () => { + // bottomObserver.disconnect(); + // headerObserver.disconnect(); + // stickyScrollbarRef.current?.removeEventListener('scroll', handleScroll); + // window.removeEventListener('resize', syncWidths); + // }; + // }, [isLoading, showSingleScrollbar]); + + +useEffect(()=>{ + console.log(stickyScrollbarRef.current?.scrollLeft, outerTableRef.current?.scrollLeft, tableContainer.current?.scrollLeft); + +},[stickyScrollbarRef.current?.scrollLeft]) /** * add the top horizontal scroll if needed */ @@ -380,6 +545,7 @@ const RecordsetTable = ({ ) } + console.log(columnModels.length); const renderColumnHeaders = () => { return columnModels.map((col: any, index: number) => { @@ -519,9 +685,9 @@ const RecordsetTable = ({ className='chaise-table-top-scroll-wrapper'>
-
- - +
+
+ {showActionButtons && renderActionsHeader()} {renderColumnHeaders()} @@ -536,6 +702,17 @@ const RecordsetTable = ({ top scrollbar when the bottom one is visible */}
+
+ + + {showActionButtons && renderActionsHeader()} + {renderColumnHeaders()} + + +
+
+ {!hasTimeoutError && numHiddenRecords > 0 &&