Skip to content

Commit ca35db7

Browse files
committed
fix: resolve all known issues ahead of 7.1.0 release
- Fix inverse sentinel placement: sentinel now renders before children when inverse=true so the IO top-margin fires correctly when scrolling up - Add SSR guard: typeof IntersectionObserver === 'undefined' check in Effect 2b prevents crash in Next.js App Router server components - Fix onScroll prop type: MouseEvent -> UIEvent (scroll events are UIEvents) - Fix defaultThreshold.value: 0.8 -> 80 (was computing 99.2% rootMargin instead of 20% on invalid scrollThreshold input) - Add exports field to package.json for Node ESM and Next.js 13+ App Router subpath resolution; main/module kept for older bundler fallback - Bump version to 7.1.0 - Fix pre-existing ts-check breakage: replace global with globalThis in test files, add resolveJsonModule for JSON import, exclude stories from tsconfig (storybook types not installed in devDependencies)
1 parent a89dda7 commit ca35db7

9 files changed

Lines changed: 67 additions & 18 deletions

File tree

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
{
22
"name": "react-infinite-scroll-component",
3-
"version": "7.0.1",
3+
"version": "7.1.0",
44
"description": "An Infinite Scroll component in react.",
55
"engines": {
66
"node": ">=20.0.0"
77
},
88
"source": "src/index.tsx",
99
"main": "dist/index.js",
10-
"unpkg": "dist/index.umd.js",
1110
"module": "dist/index.es.js",
11+
"unpkg": "dist/index.umd.js",
1212
"types": "dist/index.d.ts",
13+
"exports": {
14+
".": {
15+
"import": "./dist/index.es.js",
16+
"require": "./dist/index.js",
17+
"types": "./dist/index.d.ts"
18+
}
19+
},
1320
"scripts": {
1421
"build": "rimraf dist && rollup -c",
1522
"prepublish": "yarn build",

src/__tests__/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('React Infinite Scroll Component', () => {
6161

6262
it('calls scroll handler if provided, when user scrolls', () => {
6363
jest.useFakeTimers();
64-
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
64+
const setTimeoutSpy = jest.spyOn(globalThis, 'setTimeout');
6565
const onScrollMock = jest.fn();
6666

6767
const { container } = render(

src/__tests__/inverse.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,49 @@ describe('inverse mode triggers next near top', () => {
5151
// inverse mode: margin applies to top, so rootMargin starts with a non-zero value
5252
expect(options.rootMargin).toBe('20% 0px 0px 0px');
5353
});
54+
55+
it('renders sentinel as first child in inverse mode', () => {
56+
const { container } = render(
57+
<InfiniteScroll
58+
dataLength={5}
59+
loader={'Loading...'}
60+
hasMore={true}
61+
next={() => {}}
62+
height={100}
63+
inverse
64+
>
65+
<div id="child" />
66+
</InfiniteScroll>
67+
);
68+
69+
const inner = container.querySelector(
70+
'.infinite-scroll-component'
71+
) as HTMLElement;
72+
// sentinel must be the first DOM child so the IO top-margin fires correctly
73+
expect(inner.firstElementChild).toBe(
74+
MockIntersectionObserver.instances[0].observedElements[0]
75+
);
76+
});
77+
78+
it('renders sentinel as last child in normal (non-inverse) mode', () => {
79+
const { container } = render(
80+
<InfiniteScroll
81+
dataLength={5}
82+
loader={'Loading...'}
83+
hasMore={true}
84+
next={() => {}}
85+
height={100}
86+
>
87+
<div id="child" />
88+
</InfiniteScroll>
89+
);
90+
91+
const inner = container.querySelector(
92+
'.infinite-scroll-component'
93+
) as HTMLElement;
94+
// sentinel must be the last DOM child for the IO bottom-margin to work
95+
expect(inner.lastElementChild).toBe(
96+
MockIntersectionObserver.instances[0].observedElements[0]
97+
);
98+
});
5499
});

src/__tests__/package.test.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
export {};
2-
31
/**
42
* Validates package.json fields that affect consumers at install time.
53
* These checks run on every `yarn test` invocation — no extra infrastructure needed.
@@ -8,11 +6,7 @@ export {};
86
* that block React 18/19 consumers at npm install, like #419.
97
*/
108

11-
// eslint-disable-next-line @typescript-eslint/no-require-imports
12-
const pkg = require('../../package.json') as {
13-
peerDependencies: Record<string, string>;
14-
devDependencies: Record<string, string>;
15-
};
9+
import pkg from '../../package.json';
1610

1711
describe('package.json — peer dependency ranges', () => {
1812
it('react peer dep uses >= (open-ended), not ^ (caret-bounded)', () => {

src/__tests__/setup/intersectionObserverMock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class MockIntersectionObserver {
5050
}
5151

5252
// Assign globally so `new IntersectionObserver(...)` in the component resolves to the mock.
53-
Object.defineProperty(global, 'IntersectionObserver', {
53+
Object.defineProperty(globalThis, 'IntersectionObserver', {
5454
writable: true,
5555
configurable: true,
5656
value: MockIntersectionObserver,

src/__tests__/threshold.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ describe('parseThreshold', () => {
2929
it('warns and returns default for invalid string', () => {
3030
const t = parseThreshold('foo' as any);
3131
expect(t.unit).toBe(ThresholdUnits.Percent);
32-
expect(t.value).toBe(0.8);
32+
expect(t.value).toBe(80);
3333
expect(warnSpy).toHaveBeenCalled();
3434
});
3535

3636
it('warns and returns default for non-string/number', () => {
3737
const t = parseThreshold(null as unknown as any);
3838
expect(t.unit).toBe(ThresholdUnits.Percent);
39-
expect(t.value).toBe(0.8);
39+
expect(t.value).toBe(80);
4040
expect(warnSpy).toHaveBeenCalled();
4141
});
4242
});

src/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface Props {
2727
releaseToRefreshContent?: ReactNode;
2828
pullDownToRefreshThreshold?: number;
2929
refreshFunction?: Fn;
30-
onScroll?: (e: MouseEvent) => any;
30+
onScroll?: (e: UIEvent) => any;
3131
dataLength: number;
3232
initialScrollY?: number;
3333
className?: string;
@@ -145,6 +145,7 @@ export default function InfiniteScroll({
145145
// direction changes — typically never after initial mount.
146146
useEffect(() => {
147147
if (!hasMore) return;
148+
if (typeof IntersectionObserver === 'undefined') return;
148149

149150
const sentinel = sentinelRef.current;
150151
if (!sentinel) return;
@@ -181,7 +182,7 @@ export default function InfiniteScroll({
181182
if (!scrollEl) return;
182183

183184
const handler = (e: Event) => {
184-
setTimeout(() => onScroll(e as MouseEvent), 0);
185+
setTimeout(() => onScroll(e as UIEvent), 0);
185186
};
186187

187188
scrollEl.addEventListener('scroll', handler as EventListener);
@@ -329,6 +330,7 @@ export default function InfiniteScroll({
329330
ref={infScrollRef}
330331
style={containerStyle}
331332
>
333+
{inverse && sentinel}
332334
{pullDownToRefresh && (
333335
<div style={{ position: 'relative' }} ref={pullDownRef}>
334336
<div
@@ -348,7 +350,7 @@ export default function InfiniteScroll({
348350
{children}
349351
{!showLoader && !hasChildrenResolved && hasMore && loader}
350352
{showLoader && hasMore && loader}
351-
{sentinel}
353+
{!inverse && sentinel}
352354
{!hasMore && endMessage}
353355
</div>
354356
</div>

src/utils/threshold.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const ThresholdUnits = {
55

66
const defaultThreshold = {
77
unit: ThresholdUnits.Percent,
8-
value: 0.8,
8+
value: 80,
99
};
1010

1111
export function parseThreshold(scrollThreshold: string | number) {

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
] /* Type declaration files to be included in compilation. */,
4949
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
5050
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
51+
"resolveJsonModule": true /* Allow importing .json files (used in package.test.ts). */,
5152
"skipLibCheck": true,
5253
"forceConsistentCasingInFileNames": true
5354
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
@@ -61,5 +62,5 @@
6162
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
6263
},
6364
"include": ["src/**/*", "lint-staged.config.js", "jest.config.js"],
64-
"exclude": ["node_modules", "dist"]
65+
"exclude": ["node_modules", "dist", "src/stories"]
6566
}

0 commit comments

Comments
 (0)