diff --git a/.changeset/thick-clocks-applaud.md b/.changeset/thick-clocks-applaud.md new file mode 100644 index 00000000000..676b4fdbf64 --- /dev/null +++ b/.changeset/thick-clocks-applaud.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': patch +--- + +Fixed Scrollable component to match existing onScrolledToBottom logic diff --git a/polaris-react/src/components/Scrollable/Scrollable.tsx b/polaris-react/src/components/Scrollable/Scrollable.tsx index 0edb4ab38b4..6d92c4f79ed 100644 --- a/polaris-react/src/components/Scrollable/Scrollable.tsx +++ b/polaris-react/src/components/Scrollable/Scrollable.tsx @@ -67,6 +67,7 @@ export function Scrollable({ requestAnimationFrame(() => { const {scrollTop, clientHeight, scrollHeight} = currentScrollArea; + const canScroll = Boolean(scrollHeight > clientHeight); const isBelowTopOfScroll = Boolean(scrollTop > 0); const isAtBottomOfScroll = Boolean( scrollTop + clientHeight >= scrollHeight - LOW_RES_BUFFER, @@ -75,7 +76,7 @@ export function Scrollable({ setTopShadow(isBelowTopOfScroll); setBottomShadow(!isAtBottomOfScroll); - if (isAtBottomOfScroll && onScrolledToBottom) { + if (canScroll && isAtBottomOfScroll && onScrolledToBottom) { onScrolledToBottom(); } }); diff --git a/polaris-react/src/components/Scrollable/tests/Scrollable.test.tsx b/polaris-react/src/components/Scrollable/tests/Scrollable.test.tsx index 5572dbcb86a..c04bdc41de3 100644 --- a/polaris-react/src/components/Scrollable/tests/Scrollable.test.tsx +++ b/polaris-react/src/components/Scrollable/tests/Scrollable.test.tsx @@ -3,8 +3,24 @@ import {mountWithApp} from 'tests/utilities'; import {Scrollable} from '../Scrollable'; import {ScrollableContext} from '../context'; +import type {ScrollableProps} from '../Scrollable'; describe('', () => { + let rafSpy: jest.SpyInstance; + + beforeAll(() => { + rafSpy = jest + .spyOn(window, 'requestAnimationFrame') + .mockImplementation((cb) => { + cb(Date.now()); + return Math.random(); + }); + }); + + afterAll(() => { + rafSpy.mockRestore(); + }); + it('mounts', () => { const scrollable = mountWithApp(); expect(scrollable).toBeDefined(); @@ -71,4 +87,60 @@ describe('', () => { tabIndex: 0, }); }); + + it('calls onScrolledToBottom when scrolled to bottom', () => { + const onScrolledToBottom = jest.fn(); + + const scrollArea = mountWithApp( + +

Hello

+
, + ); + + const scrollNode = scrollArea.find('div', { + 'data-test-id': 'scroll-element', + } as ScrollableProps)?.domNode!; + + // defineProperty needed to assign values to readonly node properties + Object.defineProperty(scrollNode, 'clientHeight', {get: () => 0}); + Object.defineProperty(scrollNode, 'scrollHeight', {get: () => 10}); + Object.defineProperty(scrollNode, 'scrollTop', {get: () => 10}); + + scrollArea.act(() => { + scrollNode.dispatchEvent(new Event('scroll')); + }); + + expect(onScrolledToBottom).toHaveBeenCalledTimes(1); + }); + + it(`doesn't call onScrolledToBottom when the scroll area is not overflowing`, () => { + const onScrolledToBottom = jest.fn(); + + const scrollArea = mountWithApp( + +

Hello

+
, + ); + + const scrollNode = scrollArea.find('div', { + 'data-test-id': 'scroll-element', + } as ScrollableProps)?.domNode!; + + // defineProperty needed to assign values to readonly node properties + Object.defineProperty(scrollNode, 'clientHeight', {get: () => 10}); + Object.defineProperty(scrollNode, 'scrollHeight', {get: () => 10}); + Object.defineProperty(scrollNode, 'scrollTop', {get: () => 10}); + + scrollArea.act(() => { + scrollNode.dispatchEvent(new Event('scroll')); + }); + + expect(onScrolledToBottom).not.toHaveBeenCalled(); + }); });