Skip to content

Commit e339525

Browse files
committed
Fix Outline, Page and Thumbnail components crashing when placed outside Document
Closes #1709
1 parent cf5327b commit e339525

File tree

9 files changed

+87
-40
lines changed

9 files changed

+87
-40
lines changed

packages/react-pdf/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ Loads a document passed using `file` prop.
485485

486486
### Page
487487

488-
Displays a page. Should be placed inside `<Document />`. Alternatively, it can have `pdf` prop passed, which can be obtained from `<Document />`'s `onLoadSuccess` callback function, however some advanced functions like linking between pages inside a document may not be working correctly.
488+
Displays a page. Should be placed inside `<Document />`. Alternatively, it can have `pdf` prop passed, which can be obtained from `<Document />`'s `onLoadSuccess` callback function, however some advanced functions like rendering annotations and linking between pages inside a document may not be working correctly.
489489

490490
#### Props
491491

packages/react-pdf/src/Outline.spec.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('Outline', () => {
5858
});
5959

6060
describe('loading', () => {
61-
it('loads an outline and calls onLoadSuccess callback properly', async () => {
61+
it('loads an outline and calls onLoadSuccess callback properly when placed inside Document', async () => {
6262
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
6363

6464
renderWithContext(<Outline onLoadSuccess={onLoadSuccess} />, { pdf });
@@ -68,6 +68,16 @@ describe('Outline', () => {
6868
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedOutline]);
6969
});
7070

71+
it('loads an outline and calls onLoadSuccess callback properly when pdf prop is passed', async () => {
72+
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
73+
74+
render(<Outline onLoadSuccess={onLoadSuccess} pdf={pdf} />);
75+
76+
expect.assertions(1);
77+
78+
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedOutline]);
79+
});
80+
7181
it('calls onLoadError when failed to load an outline', async () => {
7282
const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback();
7383

@@ -99,7 +109,7 @@ describe('Outline', () => {
99109
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedOutline2]);
100110
});
101111

102-
it('throws an error when placed outside Document', () => {
112+
it('throws an error when placed outside Document without pdf prop passed', () => {
103113
muteConsole();
104114

105115
expect(() => render(<Outline />)).toThrow();

packages/react-pdf/src/Outline.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,6 @@ export type OutlineProps = {
6969
const Outline: React.FC<OutlineProps> = function Outline(props) {
7070
const documentContext = useDocumentContext();
7171

72-
invariant(
73-
documentContext,
74-
'Unable to find Document context. Did you wrap <Outline /> in <Document />?',
75-
);
76-
7772
const mergedProps = { ...documentContext, ...props };
7873
const {
7974
className,
@@ -85,7 +80,10 @@ const Outline: React.FC<OutlineProps> = function Outline(props) {
8580
...otherProps
8681
} = mergedProps;
8782

88-
invariant(pdf, 'Attempted to load an outline, but no document was specified.');
83+
invariant(
84+
pdf,
85+
'Attempted to load an outline, but no document was specified. Wrap <Outline /> in a <Document /> or pass explicit `pdf` prop.',
86+
);
8987

9088
const [outlineState, outlineDispatch] = useResolver<PDFOutline | null>();
9189
const { value: outline, error: outlineError } = outlineState;
@@ -189,7 +187,11 @@ const Outline: React.FC<OutlineProps> = function Outline(props) {
189187
return (
190188
<ul>
191189
{outline.map((item, itemIndex) => (
192-
<OutlineItem key={typeof item.dest === 'string' ? item.dest : itemIndex} item={item} />
190+
<OutlineItem
191+
key={typeof item.dest === 'string' ? item.dest : itemIndex}
192+
item={item}
193+
pdf={pdf}
194+
/>
193195
))}
194196
</ul>
195197
);

packages/react-pdf/src/OutlineItem.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,23 @@ type PDFOutlineItem = PDFOutline[number];
1616

1717
type OutlineItemProps = {
1818
item: PDFOutlineItem;
19+
pdf?: PDFDocumentProxy | false;
1920
};
2021

2122
export default function OutlineItem(props: OutlineItemProps) {
2223
const documentContext = useDocumentContext();
2324

24-
invariant(
25-
documentContext,
26-
'Unable to find Document context. Did you wrap <Outline /> in <Document />?',
27-
);
28-
2925
const outlineContext = useOutlineContext();
3026

3127
invariant(outlineContext, 'Unable to find Outline context.');
3228

3329
const mergedProps = { ...documentContext, ...outlineContext, ...props };
3430
const { item, linkService, onItemClick, pdf, ...otherProps } = mergedProps;
3531

36-
invariant(pdf, 'Attempted to load an outline, but no document was specified.');
32+
invariant(
33+
pdf,
34+
'Attempted to load an outline, but no document was specified. Wrap <Outline /> in a <Document /> or pass explicit `pdf` prop.',
35+
);
3736

3837
const getDestination = useCachedValue(() => {
3938
if (typeof item.dest === 'string') {
@@ -64,6 +63,11 @@ export default function OutlineItem(props: OutlineItemProps) {
6463
function onClick(event: React.MouseEvent<HTMLAnchorElement>) {
6564
event.preventDefault();
6665

66+
invariant(
67+
onItemClick || linkService,
68+
'Either onItemClick callback or linkService must be defined in order to navigate to an outline item.',
69+
);
70+
6771
if (onItemClick) {
6872
Promise.all([getDestination(), getPageIndex(), getPageNumber()]).then(
6973
([dest, pageIndex, pageNumber]) => {
@@ -74,7 +78,7 @@ export default function OutlineItem(props: OutlineItemProps) {
7478
});
7579
},
7680
);
77-
} else {
81+
} else if (linkService) {
7882
linkService.goToDestination(item.dest);
7983
}
8084
}
@@ -92,6 +96,7 @@ export default function OutlineItem(props: OutlineItemProps) {
9296
<OutlineItem
9397
key={typeof subitem.dest === 'string' ? subitem.dest : subitemIndex}
9498
item={subitem}
99+
pdf={pdf}
95100
{...otherProps}
96101
/>
97102
))}

packages/react-pdf/src/Page.spec.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('Page', () => {
8282
});
8383

8484
describe('loading', () => {
85-
it('loads a page and calls onLoadSuccess callback properly', async () => {
85+
it('loads a page and calls onLoadSuccess callback properly when placed inside Document', async () => {
8686
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
8787

8888
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
@@ -95,6 +95,23 @@ describe('Page', () => {
9595
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]);
9696
});
9797

98+
it('loads a page and calls onLoadSuccess callback properly when pdf prop is passed', async () => {
99+
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
100+
101+
render(
102+
<Page
103+
onLoadSuccess={onLoadSuccess}
104+
pageIndex={0}
105+
pdf={pdf}
106+
renderAnnotationLayer={false}
107+
/>,
108+
);
109+
110+
expect.assertions(1);
111+
112+
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]);
113+
});
114+
98115
it('returns all desired parameters in onLoadSuccess callback', async () => {
99116
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
100117
makeAsyncCallback<[PageCallback]>();
@@ -252,7 +269,7 @@ describe('Page', () => {
252269
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedPage2]);
253270
});
254271

255-
it('throws an error when placed outside Document', () => {
272+
it('throws an error when placed outside Document without pdf prop passed', () => {
256273
muteConsole();
257274

258275
expect(() => render(<Page pageIndex={0} />)).toThrow();

packages/react-pdf/src/Page.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,6 @@ export type PageProps = {
317317
const Page: React.FC<PageProps> = function Page(props) {
318318
const documentContext = useDocumentContext();
319319

320-
invariant(
321-
documentContext,
322-
'Unable to find Document context. Did you wrap <Page /> in <Document />?',
323-
);
324-
325320
const mergedProps = { ...documentContext, ...props };
326321
const {
327322
_className = 'react-pdf__Page',
@@ -371,7 +366,10 @@ const Page: React.FC<PageProps> = function Page(props) {
371366
const { value: page, error: pageError } = pageState;
372367
const pageElement = useRef<HTMLDivElement>(null);
373368

374-
invariant(pdf, 'Attempted to load a page, but no document was specified.');
369+
invariant(
370+
pdf,
371+
'Attempted to load a page, but no document was specified. Wrap <Page /> in a <Document /> or pass explicit `pdf` prop.',
372+
);
375373

376374
const pageIndex = isProvided(pageNumberProps) ? pageNumberProps - 1 : pageIndexProps ?? null;
377375

packages/react-pdf/src/Page/AnnotationLayer.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ import type { Annotations } from '../shared/types.js';
1717

1818
export default function AnnotationLayer() {
1919
const documentContext = useDocumentContext();
20-
21-
invariant(
22-
documentContext,
23-
'Unable to find Document context. Did you wrap <Page /> in <Document />?',
24-
);
25-
2620
const pageContext = usePageContext();
2721

2822
invariant(pageContext, 'Unable to find Page context.');
@@ -42,7 +36,12 @@ export default function AnnotationLayer() {
4236
scale = 1,
4337
} = mergedProps;
4438

39+
invariant(
40+
pdf,
41+
'Attempted to load page annotations, but no document was specified. Wrap <Page /> in a <Document /> or pass explicit `pdf` prop.',
42+
);
4543
invariant(page, 'Attempted to load page annotations, but no page was specified.');
44+
invariant(linkService, 'Attempted to load page annotations, but no linkService was specified.');
4645

4746
const [annotationsState, annotationsDispatch] = useResolver<Annotations>();
4847
const { value: annotations, error: annotationsError } = annotationsState;
@@ -147,7 +146,7 @@ export default function AnnotationLayer() {
147146
);
148147

149148
function renderAnnotationLayer() {
150-
if (!pdf || !page || !annotations) {
149+
if (!pdf || !page || !linkService || !annotations) {
151150
return;
152151
}
153152

packages/react-pdf/src/Thumbnail.spec.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('Thumbnail', () => {
6868
});
6969

7070
describe('loading', () => {
71-
it('loads a page and calls onLoadSuccess callback properly', async () => {
71+
it('loads a page and calls onLoadSuccess callback properly when placed inside Document', async () => {
7272
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
7373

7474
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} />, { pdf });
@@ -78,6 +78,16 @@ describe('Thumbnail', () => {
7878
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]);
7979
});
8080

81+
it('loads a page and calls onLoadSuccess callback properly when pdf prop is passed', async () => {
82+
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
83+
84+
render(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} pdf={pdf} />);
85+
86+
expect.assertions(1);
87+
88+
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]);
89+
});
90+
8191
it('returns all desired parameters in onLoadSuccess callback', async () => {
8292
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
8393
makeAsyncCallback<[PageCallback]>();
@@ -190,7 +200,7 @@ describe('Thumbnail', () => {
190200
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedThumbnail2]);
191201
});
192202

193-
it('throws an error when placed outside Document', () => {
203+
it('throws an error when placed outside Document without pdf prop passed', () => {
194204
muteConsole();
195205

196206
expect(() => render(<Thumbnail pageIndex={0} />)).toThrow();

packages/react-pdf/src/Thumbnail.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,21 @@ export type ThumbnailProps = Omit<
5252
const Thumbnail: React.FC<ThumbnailProps> = function Thumbnail(props) {
5353
const documentContext = useDocumentContext();
5454

55-
invariant(
56-
documentContext,
57-
'Unable to find Document context. Did you wrap <Page /> in <Document />?',
58-
);
59-
6055
const mergedProps = { ...documentContext, ...props };
6156
const {
6257
className,
6358
linkService,
6459
onItemClick,
6560
pageIndex: pageIndexProps,
6661
pageNumber: pageNumberProps,
62+
pdf,
6763
} = mergedProps;
6864

65+
invariant(
66+
pdf,
67+
'Attempted to load a thumbnail, but no document was specified. Wrap <Thumbnail /> in a <Document /> or pass explicit `pdf` prop.',
68+
);
69+
6970
const pageIndex = isProvided(pageNumberProps) ? pageNumberProps - 1 : pageIndexProps ?? null;
7071

7172
const pageNumber = pageNumberProps ?? (isProvided(pageIndexProps) ? pageIndexProps + 1 : null);
@@ -77,12 +78,17 @@ const Thumbnail: React.FC<ThumbnailProps> = function Thumbnail(props) {
7778
return;
7879
}
7980

81+
invariant(
82+
onItemClick || linkService,
83+
'Either onItemClick callback or linkService must be defined in order to navigate to an outline item.',
84+
);
85+
8086
if (onItemClick) {
8187
onItemClick({
8288
pageIndex,
8389
pageNumber,
8490
});
85-
} else {
91+
} else if (linkService) {
8692
linkService.goToPage(pageNumber);
8793
}
8894
}

0 commit comments

Comments
 (0)