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();
+ });
});