Skip to content

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

Open
wants to merge 19 commits into
base: master
Choose a base branch
from

Conversation

KenyShah24
Copy link
Contributor

This PR includes code changes to ensure the table header remains sticky while scrolling content on the recordset page using the Intersection Observer.

Issues Identified During Testing:

  1. E2E Testing with Docker:
  • The Docker image build process encounters compatibility issues with Node.js and npm versions required for the Chaise project.
  • Additionally, openjdk-8-sdk is missing when running the script.
    The included changes address these dependency errors
  1. E2E Testing Locally:
  • On Windows, the mkdir -p command is not functioning as expected.
  • A solution for this issue is still being explored.

@KenyShah24 KenyShah24 requested a review from RFSH February 11, 2025 17:49
@RFSH RFSH self-assigned this Feb 11, 2025
Copy link
Member

@RFSH RFSH left a comment

Choose a reason for hiding this comment

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

The implemented solution is not responsive and therefore we cannot merge it as is.

The vertical position of the sticky header is calculated on the initial load and remains the same. So if the user squeezes the page or remove a banner/alert (which changes the height of the top section), the sticky header shows up in the wrong spot.

Also there's no reason to keep the height in the React state. It's only used for manipulating the position of sticky header and not used in the render logic, so we shouldn't add it to state.

Other changes that should be applied:

  • Please remove .last-run.json.
  • Revert the docker file changes that you did.
  • Revert the changes in locators/recordset.ts. Instead of using filter, we just need to change the selector to use the :not(.sticky-header) stynax. You can just revert you changes. Once we think this PR is ready, I'll take care of test cases myself.

Copy link
Member

@RFSH RFSH left a comment

Choose a reason for hiding this comment

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

We had a call about this and I'm going to summarize what we talked about here:

The attachContainerHeightSensors function already does the calculations that we need and so we shouldn't add another resizeSensor for doing similar calculations.

Instead we can add a callback as an argument to attachContainerHeightSensors which would tell us about the height changes. And then we can use that callback for setting the top of the sticky header.

Copy link
Member

@RFSH RFSH left a comment

Choose a reason for hiding this comment

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

With the latest changes, the sticky header is now properly adjusting as the available space above the table changes. The width of the headers are also adjusting properly.

But there are still two functionality issues that I mentioned in my comments and I'll summarize here:

  1. The top calculation is not working properly for the popups. It's not honoring the vertical space outside of the modal, so the sticky header shows up on top of the page size dropdown.

  2. This implementation is forcing React to rerender the recordset component and all its children after each height/width change. This can cause performance issues on busier pages.

By the way, please update this branch with the latest changes of the main branch.

@@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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

/**
 * Determines the top for the sticky header
 */
headerTop?: number,

position: absolute !important;
}
.sticky-header {
Copy link
Member

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.

@@ -423,6 +495,15 @@ const RecordsetTable = ({
})
}

const renderHeader = () => {
return(
<tr>
Copy link
Member

Choose a reason for hiding this comment

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

Fix the indentation to match the rest of the file.

@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

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

Don't use any. Create a proper type here and for the state variable in src/components/recordset/recordset.tsx import that type:

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.

@@ -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');
Copy link
Member

Choose a reason for hiding this comment

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

Add this to CLASS_NAMES in src/utils/constants.ts.

@@ -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}>
Copy link
Member

Choose a reason for hiding this comment

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

Create a variable for config.displayMode.indexOf(RecordsetDisplayMode.RELATED) !== 0 at the beginning of this component. Call it something like enableStickyHeader. And then use this boolean for all the different places that you have the sticky header logic.

Copy link
Member

Choose a reason for hiding this comment

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

Remove the id attribute.

if (onContainerHeightChange) {
//To notify height change
onContainerHeightChange({
top: containerSticky.offsetHeight + parentContainerSticky.offsetHeight,
Copy link
Member

Choose a reason for hiding this comment

The 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 isa:dataset table (https://dev.derivacloud.org/chaise/recordset/#1/isa:dataset@sort(accession::desc::). For example the Title facet is a good example.

const syncWidths = () => {
if (stickyHeaderRef.current && tableRef.current) {

const originalThs = tableRef.current.querySelectorAll('tbody > tr > td');
Copy link
Member

Choose a reason for hiding this comment

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

Why is this selecting tds? and not th?

@@ -367,7 +369,8 @@ const RecordsetInner = ({
}

// handle the scrollable container
const resizeSensors = attachContainerHeightSensors(parentContainer, parentStickyArea);
const resizeSensors = attachContainerHeightSensors(parentContainer, parentStickyArea, undefined, setTopPanelDimensions);
Copy link
Member

Choose a reason for hiding this comment

The 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 setTopPanelDimensions and passing the state variable to RecordsetTable, is there anyway that we can pass the top value from here? Maybe attach it as a data-* attribute to an element and then grab it from that page?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants