Skip to content

Commit

Permalink
fix(Groups): update sticky observer on attach/detach
Browse files Browse the repository at this point in the history
  • Loading branch information
marstamm committed May 9, 2023
1 parent d4843e9 commit 51903eb
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 32 deletions.
75 changes: 44 additions & 31 deletions src/hooks/useStickyIntersectionObserver.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {
useEffect
useCallback,
useEffect,
useState
} from 'preact/hooks';

import {
query as domQuery
} from 'min-dom';

import { useEvent } from './useEvent';


/**
* @callback setSticky
Expand All @@ -23,55 +27,64 @@ import {
* @param {setSticky} setSticky
*/
export function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky) {

const [ scrollContainer, setScrollContainer ] = useState(domQuery(scrollContainerSelector));

const updateScrollContainer = useCallback(() => {
const newScrollContainer = domQuery(scrollContainerSelector);

if (newScrollContainer !== scrollContainer) {
setScrollContainer(newScrollContainer);
}
}, [ scrollContainerSelector, scrollContainer ]);

useEffect(() => {
updateScrollContainer();
}, [ updateScrollContainer ]);

useEvent('propertiesPanel.attach', updateScrollContainer);
useEvent('propertiesPanel.detach', updateScrollContainer);

useEffect(() => {
const Observer = IntersectionObserver;

// return early if IntersectionObserver is not available
if (!Observer) {
return;
}

let observer;
// TODO(@barmac): test this
if (!ref.current || !scrollContainer) {
return;
}

if (ref.current) {
const scrollContainer = domQuery(scrollContainerSelector);
const observer = new Observer((entries) => {

// TODO(@barmac): test this
if (!scrollContainer) {
// The ScrollContainer is unmounted, do not update sticky state
if (scrollContainer.scrollHeight === 0) {
return;
}

observer = new Observer((entries) => {

// The ScrollContainer is unmounted, do not update sticky state
if (scrollContainer.scrollHeight === 0) {
return;
entries.forEach(entry => {
if (entry.intersectionRatio < 1) {
setSticky(true);
}
else if (entry.intersectionRatio === 1) {
setSticky(false);
}

entries.forEach(entry => {
if (entry.intersectionRatio < 1) {
setSticky(true);
}
else if (entry.intersectionRatio === 1) {
setSticky(false);
}
});
},
{
root: scrollContainer,
rootMargin: '0px 0px 999999% 0px', // Use bottom margin to avoid stickyness when scrolling out to bottom
threshold: [ 1 ]
});
observer.observe(ref.current);
}
},
{
root: scrollContainer,
rootMargin: '0px 0px 999999% 0px', // Use bottom margin to avoid stickyness when scrolling out to bottom
threshold: [ 1 ]
});
observer.observe(ref.current);

// Unobserve if unmounted
return () => {
if (ref.current && observer) {
observer.unobserve(ref.current);
}
observer.unobserve(ref.current);
};

}, [ ref.current, scrollContainerSelector, setSticky ]);
}, [ ref.current, scrollContainer, setSticky ]);
}
96 changes: 95 additions & 1 deletion test/spec/hooks/useStickyIntersectionObserver.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import EventBus from 'diagram-js/lib/core/EventBus';

import { useStickyIntersectionObserver } from 'src/hooks';

import { waitFor } from '@testing-library/preact';
import { renderHook } from '@testing-library/preact-hooks';

import TestContainer from 'mocha-test-container-support';
import { EventContext } from '../../../src/context';

describe('hooks/userStickyIntersectionObserver', function() {

Expand All @@ -13,8 +17,16 @@ describe('hooks/userStickyIntersectionObserver', function() {
container = TestContainer.get(this);
});

let eventBus;

beforeEach(function() {
eventBus = new EventBus();
});

afterEach(function() {
global.IntersectionObserver = OriginalIntersectionObserver;

container.remove();
});


Expand Down Expand Up @@ -124,6 +136,40 @@ describe('hooks/userStickyIntersectionObserver', function() {
});


it('should observe after being attached', async function() {

// given
const observeSpy = sinon.spy();

mockIntersectionObserver({ observe: observeSpy });

const domObject = <div></div>;
const ref = { current: domObject };

await renderHook(() => {
useStickyIntersectionObserver(ref, '#scrollContainer', () => {});

return domObject;
}, { wrapper: WithEventContext(eventBus) });

// assume
expect(observeSpy).not.to.have.been.called;

// when
const scrollContainer = document.createElement('div');
scrollContainer.setAttribute('id', 'scrollContainer');
container.appendChild(scrollContainer);

eventBus.fire('propertiesPanel.attach');

// then
await waitFor(() => {
expect(observeSpy).to.have.been.calledOnce;
});

});


it('should unobserve after unmount', async function() {

// given
Expand All @@ -149,6 +195,38 @@ describe('hooks/userStickyIntersectionObserver', function() {
});


it('should unobserve after being detached', async function() {

// given
const unobserveSpy = sinon.spy();

mockIntersectionObserver({ unobserve: unobserveSpy });

const scrollContainer = document.createElement('div');
scrollContainer.setAttribute('id', 'scrollContainer');
container.appendChild(scrollContainer);

const domObject = <div></div>;
const ref = { current: domObject };

await renderHook(() => {
useStickyIntersectionObserver(ref, '#scrollContainer', () => {});

return domObject;
}, { wrapper: WithEventContext(eventBus) });

// when
scrollContainer.remove();
eventBus.fire('propertiesPanel.detach');

// then
await waitFor(() => {
expect(unobserveSpy).to.have.been.calledOnce;
});

});


it('should NOT crash when IntersectionObserver is not available', async function() {

// given
Expand Down Expand Up @@ -216,4 +294,20 @@ function mockIntersectionObserver(props) {
global.IntersectionObserver = MockObserver;

return triggerCallbacks;
}
}

function WithEventContext(eventBus) {
return function Wrapper(props) {
const { children } = props;

const eventContext = {
eventBus
};

return (
<EventContext.Provider value={ eventContext }>
{ children }
</EventContext.Provider>
);
};
}

0 comments on commit 51903eb

Please sign in to comment.