Skip to content

Commit

Permalink
fix(ObjectPage): scroll to section when programmatically selected (#6768
Browse files Browse the repository at this point in the history
)

Fixes #6765
  • Loading branch information
Lukas742 authored Jan 9, 2025
1 parent 7301261 commit ad9937a
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 56 deletions.
88 changes: 80 additions & 8 deletions packages/main/src/components/ObjectPage/ObjectPage.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,64 @@ describe('ObjectPage', () => {
cy.findByText('First Content').should('be.visible');
});

it('programmatic prop selection', () => {
const TestComp = (props: ObjectPagePropTypes) => {
const [selectedSection, setSelectedSection] = useState(props.selectedSectionId);
const [selectedSubSection, setSelectedSubSection] = useState(props.selectedSubSectionId);

return (
<>
<button
onClick={() => {
setSelectedSection('goals');
}}
>
Select Goals
</button>
<button
onClick={() => {
setSelectedSubSection('personal-payment-information');
}}
>
Select Payment Information
</button>
<ObjectPage
{...props}
selectedSubSectionId={selectedSubSection}
selectedSectionId={selectedSection}
style={{ height: '1000px', scrollBehavior: 'auto' }}
>
{OPContent}
</ObjectPage>
</>
);
};

[{ titleArea: DPTitle, headerArea: DPContent }, { titleArea: DPTitle }, { headerArea: DPContent }, {}].forEach(
(props: ObjectPagePropTypes) => {
cy.mount(<TestComp {...props} selectedSubSectionId={`employment-job-relationship`} />);
cy.findByText('employment-job-relationship-content').should('be.visible');
cy.findByText('Job Information').should('not.be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Employment').should('have.attr', 'aria-selected', 'true');

cy.mount(<TestComp {...props} selectedSectionId={`personal`} />);
cy.findByText('personal-connect-content').should('be.visible');
cy.findByText('test-content').should('not.be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Personal').should('have.attr', 'aria-selected', 'true');

cy.findByText('Select Goals').click();
cy.findByText('goals-content').should('be.visible');
cy.findByText('personal-connect-content').should('not.be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Goals').should('have.attr', 'aria-selected', 'true');

cy.findByText('Select Payment Information').click();
cy.findByText('personal-payment-information-content').should('be.visible');
cy.findByText('personal-connect-content').should('not.be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Personal').should('have.attr', 'aria-selected', 'true');
}
);
});

cypressPassThroughTestsFactory(ObjectPage);
});

Expand All @@ -1073,7 +1131,7 @@ const DPTitle = (
<Breadcrumbs>
<BreadcrumbsItem>Manager Cockpit</BreadcrumbsItem>
<BreadcrumbsItem>My Team</BreadcrumbsItem>
<BreadcrumbsItem>Employee Details</BreadcrumbsItem>
<BreadcrumbsItem>Employee Details (Denise Smith)</BreadcrumbsItem>
</Breadcrumbs>
}
>
Expand Down Expand Up @@ -1101,10 +1159,14 @@ const DPContent = (

const OPContent = [
<ObjectPageSection key="0" titleText="Goals" id="goals" aria-label="Goals">
<div data-testid="section 1" style={{ height: '400px', width: '100%', background: 'lightblue' }} />
<div data-testid="section 1" style={{ height: '400px', width: '100%', background: 'lightblue' }}>
<span data-testid="goals-content">goals-content</span>
</div>
</ObjectPageSection>,
<ObjectPageSection key="1" titleText="Test" id="test" aria-label="Test">
<div data-testid="section 2" style={{ height: '1200px', width: '100%', background: 'lightyellow' }}></div>
<div data-testid="section 2" style={{ height: '1200px', width: '100%', background: 'lightyellow' }}>
<span data-testid="test-content">test-content</span>
</div>
</ObjectPageSection>,
<ObjectPageSection key="2" titleText="Personal" id="personal" aria-label="Personal">
<ObjectPageSubSection
Expand All @@ -1121,25 +1183,35 @@ const OPContent = [
</>
}
>
<div style={{ height: '400px', width: '100%', background: 'black' }} />
<div style={{ height: '400px', width: '100%', background: 'black' }}>
<span data-testid="personal-connect-content">personal-connect-content</span>
</div>
</ObjectPageSubSection>
<ObjectPageSubSection
titleText="Payment Information"
id="personal-payment-information"
aria-label="Payment Information"
>
<div style={{ height: '400px', width: '100%', background: 'blue' }} />
<div style={{ height: '400px', width: '100%', background: 'blue' }}>
<span data-testid="personal-payment-information-content">personal-payment-information-content</span>
</div>
</ObjectPageSubSection>
</ObjectPageSection>,
<ObjectPageSection key="3" titleText="Employment" id={`~\`!1@#$%^&*()-_+={}[]:;"'z,<.>/?|♥`} aria-label="Employment">
<ObjectPageSubSection titleText="Job Information" id="employment-job-information" aria-label="Job Information">
<div style={{ height: '100px', width: '100%', background: 'orange' }}></div>
<div style={{ height: '100px', width: '100%', background: 'orange' }}>
<span data-testid="employment-job-information-content">employment-job-information-content</span>
</div>
</ObjectPageSubSection>
<ObjectPageSubSection titleText="Employee Details" id="employment-employee-details" aria-label="Employee Details">
<div style={{ height: '100px', width: '100%', background: 'cadetblue' }}></div>
<div style={{ height: '100px', width: '100%', background: 'cadetblue' }}>
<span data-testid="employment-employee-details-content">employment-employee-details-content</span>
</div>
</ObjectPageSubSection>
<ObjectPageSubSection titleText="Job Relationship" id="employment-job-relationship" aria-label="Job Relationship">
<div style={{ height: '100px', width: '100%', background: 'lightgrey' }}></div>
<div style={{ height: '100px', width: '100%', background: 'lightgrey' }}>
<span data-testid="employment-job-relationship-content">employment-job-relationship-content</span>
</div>
</ObjectPageSubSection>
</ObjectPageSection>
];
Expand Down
6 changes: 6 additions & 0 deletions packages/main/src/components/ObjectPage/ObjectPageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ export const getSectionById = (sections, id) => {
);
});
};

export const getSectionElementById = (objectPage: HTMLDivElement, isSubSection: boolean, id: string | undefined) => {
return objectPage?.querySelector<HTMLElement>(
`#${isSubSection ? 'ObjectPageSubSection' : 'ObjectPageSection'}-${CSS.escape(id)}`
);
};
71 changes: 23 additions & 48 deletions packages/main/src/components/ObjectPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type {
} from '../ObjectPageTitle/index.js';
import { CollapsedAvatar } from './CollapsedAvatar.js';
import { classNames, styleData } from './ObjectPage.module.css.js';
import { getSectionById } from './ObjectPageUtils.js';
import { getSectionById, getSectionElementById } from './ObjectPageUtils.js';

const ObjectPageCssVariables = {
headerDisplay: '--_ui5wcr_ObjectPage_header_display',
Expand Down Expand Up @@ -218,10 +218,10 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
const [internalSelectedSectionId, setInternalSelectedSectionId] = useState<string | undefined>(
selectedSectionId ?? firstSectionId
);
const [selectedSubSectionId, setSelectedSubSectionId] = useState(props.selectedSubSectionId);
const [selectedSubSectionId, setSelectedSubSectionId] = useState(undefined);
const [headerPinned, setHeaderPinned] = useState(headerPinnedProp);
const isProgrammaticallyScrolled = useRef(false);
const prevSelectedSectionId = useRef<string | undefined>(undefined);
const [isMounted, setIsMounted] = useState(false);

const [componentRef, objectPageRef] = useSyncRef(ref);
const topHeaderRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -326,9 +326,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
}, [image, classNames.headerImage, classNames.image, imageShapeCircle]);

const scrollToSectionById = (id: string | undefined, isSubSection = false) => {
const section = objectPageRef.current?.querySelector<HTMLElement>(
`#${isSubSection ? 'ObjectPageSubSection' : 'ObjectPageSection'}-${CSS.escape(id)}`
);
const section = getSectionElementById(objectPageRef.current, isSubSection, id);
scrollTimeout.current = performance.now() + 500;
if (section) {
const safeTopHeaderHeight = topHeaderHeight || prevTopHeaderHeight.current;
Expand Down Expand Up @@ -367,45 +365,6 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
isProgrammaticallyScrolled.current = false;
};

const programmaticallySetSection = () => {
const currentId = selectedSectionId ?? firstSectionId;
if (currentId !== prevSelectedSectionId.current) {
debouncedOnSectionChange.cancel();
isProgrammaticallyScrolled.current = true;
setInternalSelectedSectionId(currentId);
prevSelectedSectionId.current = currentId;
const sectionNodes = objectPageRef.current?.querySelectorAll('section[data-component-name="ObjectPageSection"]');
const currentIndex = childrenArray.findIndex((objectPageSection) => {
return isValidElement(objectPageSection) && objectPageSection.props?.id === currentId;
});
fireOnSelectedChangedEvent({}, currentIndex, currentId, sectionNodes[0]);
}
};

// change selected section when prop is changed (external change)
const [timeStamp, setTimeStamp] = useState(0);
const requestAnimationFrameRef = useRef<undefined | number>(undefined);
useEffect(() => {
if (selectedSectionId) {
if (mode === ObjectPageMode.Default) {
// wait for DOM draw, otherwise initial scroll won't work as intended
if (timeStamp < 750 && timeStamp !== undefined) {
requestAnimationFrameRef.current = requestAnimationFrame((internalTimestamp) => {
setTimeStamp(internalTimestamp);
});
} else {
setTimeStamp(undefined);
programmaticallySetSection();
}
} else {
programmaticallySetSection();
}
}
return () => {
cancelAnimationFrame(requestAnimationFrameRef.current);
};
}, [timeStamp, selectedSectionId, firstSectionId, debouncedOnSectionChange]);

// section was selected by clicking on the tab bar buttons
const handleOnSectionSelected = (targetEvent, newSelectionSectionId, index, section) => {
isProgrammaticallyScrolled.current = true;
Expand All @@ -421,12 +380,24 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
fireOnSelectedChangedEvent(targetEvent, index, newSelectionSectionId, section);
};

useEffect(() => {
if (selectedSectionId) {
const selectedSection = getSectionElementById(objectPageRef.current, false, selectedSectionId);
if (selectedSection) {
const selectedSectionIndex = Array.from(
selectedSection.parentElement.querySelectorAll(':scope > [data-component-name="ObjectPageSection"]')
).indexOf(selectedSection);
handleOnSectionSelected({}, selectedSectionId, selectedSectionIndex, selectedSection);
}
}
}, [selectedSectionId]);

// do internal scrolling
useEffect(() => {
if (mode === ObjectPageMode.Default && isProgrammaticallyScrolled.current === true && !selectedSubSectionId) {
scrollToSection(internalSelectedSectionId);
}
}, [internalSelectedSectionId, mode, isProgrammaticallyScrolled, scrollToSection, selectedSubSectionId]);
}, [internalSelectedSectionId, mode, selectedSubSectionId]);

// Scrolling for Sub Section Selection
useEffect(() => {
Expand Down Expand Up @@ -457,11 +428,15 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
}, [headerPinned, topHeaderHeight]);

useEffect(() => {
if (!isMounted) {
setIsMounted(true);
return;
}
setSelectedSubSectionId(props.selectedSubSectionId);
if (props.selectedSubSectionId) {
isProgrammaticallyScrolled.current = true;
if (mode === ObjectPageMode.IconTabBar) {
let sectionId;
let sectionId: string;
childrenArray.forEach((section) => {
if (isValidElement(section) && section.props && section.props.children) {
safeGetChildrenArray(section.props.children).forEach((subSection) => {
Expand All @@ -480,7 +455,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
}
}
}
}, [props.selectedSubSectionId, childrenArray, mode]);
}, [props.selectedSubSectionId, isMounted]);

const tabContainerContainerRef = useRef(null);
useEffect(() => {
Expand Down

0 comments on commit ad9937a

Please sign in to comment.