AAE-43707 Add storybook stories for pdf viewer family

Adds Storybook coverage for pdf-viewer, pdf-viewer-thumb and
pdf-viewer-thumbnails. The thumbnail stories use lightweight mocks
that emulate the pdf.js PDFViewer page descriptor surface so the
panels can be rendered in isolation.
This commit is contained in:
Wojciech Duda
2026-05-04 17:19:55 +02:00
parent 8d07c647b7
commit 49f1553655
3 changed files with 388 additions and 0 deletions
@@ -0,0 +1,119 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { applicationConfig, Meta, StoryObj, moduleMetadata } from '@storybook/angular';
import { PdfThumbComponent } from './pdf-viewer-thumb.component';
import { provideStoryCore } from '../../../stories/core-story.providers';
const buildMockPage = (id: number, color: string) => ({
id,
getWidth: () => 91,
getHeight: () => 114,
getPage: () =>
Promise.resolve({
getViewport: ({ scale }: { scale: number }) => ({ width: 91 * scale, height: 114 * scale }),
render: ({ canvasContext, viewport }: { canvasContext: CanvasRenderingContext2D; viewport: { width: number; height: number } }) => {
canvasContext.fillStyle = color;
canvasContext.fillRect(0, 0, viewport.width, viewport.height);
canvasContext.fillStyle = '#333';
canvasContext.font = '14px sans-serif';
canvasContext.fillText(`Page ${id}`, 16, 24);
return { promise: Promise.resolve() };
}
})
});
const EMPTY_PAGE = {
id: 0,
getWidth: () => 91,
getHeight: () => 114,
getPage: () => new Promise(() => undefined)
};
type PdfThumbStoryArgs = PdfThumbComponent & {
selected?: boolean;
};
const meta: Meta<PdfThumbStoryArgs> = {
component: PdfThumbComponent,
title: 'Core/Viewer/PDF Viewer Thumb',
decorators: [
moduleMetadata({
imports: [PdfThumbComponent]
}),
applicationConfig({
providers: [...provideStoryCore()]
})
],
parameters: {
docs: {
description: {
component: `Single thumbnail tile rendered for a page in the PDF Viewer thumbnails panel. Renders a small canvas preview from the supplied \`page\` descriptor.`
}
}
},
argTypes: {
page: {
control: false,
description: 'Page descriptor providing `getPage()`, `getWidth()` and `getHeight()` used to render the thumbnail.',
table: { type: { summary: 'PdfThumbnailPage' } }
},
selected: {
control: 'boolean',
description: 'Visual flag toggled by the parent thumbnails list when the page is the active one.',
table: { category: 'Storybook Helpers' }
}
}
};
export default meta;
type Story = StoryObj<PdfThumbStoryArgs>;
export const Empty: Story = {
args: {
page: EMPTY_PAGE,
selected: false
},
render: (args) => ({
props: args,
template: `<adf-pdf-thumb [page]="page" style="display:inline-block; padding:8px;" />`
})
};
export const Populated: Story = {
args: {
page: buildMockPage(1, '#e3f2fd'),
selected: false
},
render: (args) => ({
props: args,
template: `<adf-pdf-thumb [page]="page" style="display:inline-block; padding:8px;" />`
})
};
export const Selected: Story = {
args: {
page: buildMockPage(2, '#bbdefb'),
selected: true
},
render: (args) => ({
props: args,
template: `<div [style.outline]="selected ? '2px solid #1976d2' : 'none'" style="display:inline-block; padding:8px;">
<adf-pdf-thumb [page]="page" />
</div>`
})
};
@@ -0,0 +1,127 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { applicationConfig, Meta, StoryObj, moduleMetadata } from '@storybook/angular';
import { PdfThumbListComponent } from './pdf-viewer-thumbnails.component';
import { provideStoryCore } from '../../../stories/core-story.providers';
const buildPdfViewerMock = (totalPages: number) => {
const pageHandlers: Array<(event: { pageNumber: number }) => void> = [];
const buildMockPage = (id: number) => ({
id,
width: 91,
height: 114
});
const buildPageProxy = (id: number) => ({
getViewport: ({ scale }: { scale: number }) => ({ width: 91 * scale, height: 114 * scale }),
render: ({ canvasContext, viewport }: { canvasContext: CanvasRenderingContext2D; viewport: { width: number; height: number } }) => {
canvasContext.fillStyle = id % 2 ? '#e3f2fd' : '#f3e5f5';
canvasContext.fillRect(0, 0, viewport.width, viewport.height);
canvasContext.fillStyle = '#333';
canvasContext.font = '12px sans-serif';
canvasContext.fillText(`Page ${id}`, 12, 20);
return { promise: Promise.resolve() };
}
});
return {
currentPageNumber: 1,
pagesCount: totalPages,
_pages: Array.from({ length: totalPages }, (_, i) => buildMockPage(i + 1)),
pdfDocument: {
getPage: (id: number) => Promise.resolve(buildPageProxy(id))
},
eventBus: {
on: (event: string, handler: (e: { pageNumber: number }) => void) => {
if (event === 'pagechanging') {
pageHandlers.push(handler);
}
},
off: () => undefined
}
} as any;
};
type PdfThumbnailsStoryArgs = PdfThumbListComponent & {
pageCount?: number;
};
const meta: Meta<PdfThumbnailsStoryArgs> = {
component: PdfThumbListComponent,
title: 'Core/Viewer/PDF Viewer Thumbnails',
decorators: [
moduleMetadata({
imports: [PdfThumbListComponent]
}),
applicationConfig({
providers: [...provideStoryCore()]
})
],
parameters: {
docs: {
description: {
component: `Side panel listing thumbnails for every page of the PDF currently rendered by the \`adf-pdf-viewer\`. Stories use a lightweight mock of the underlying \`PDFViewer\`.`
}
}
},
argTypes: {
pdfViewer: {
control: false,
description: 'Reference to the underlying pdf.js `PDFViewer` instance. Required input.',
table: { type: { summary: 'PDFViewer' } }
},
pageCount: {
control: { type: 'number', min: 0, max: 50 },
description: 'Number of pages used to build the mock `PDFViewer`.',
table: { category: 'Storybook Helpers' }
},
close: {
action: 'close',
description: 'Emitted when the user requests to close the thumbnails panel (e.g. via Escape).',
table: { type: { summary: 'EventEmitter <void>' }, category: 'Actions' }
}
},
args: {
pageCount: 5
},
render: (args) => ({
props: {
...args,
pdfViewer: buildPdfViewerMock(args.pageCount ?? 0)
},
template: `<div style="height:480px; width:140px; border:1px solid #ddd; overflow:hidden;">
<adf-pdf-thumbnails [pdfViewer]="pdfViewer" (close)="close($event)" style="display:block; height:100%;" />
</div>`
})
};
export default meta;
type Story = StoryObj<PdfThumbnailsStoryArgs>;
export const Empty: Story = {
args: { pageCount: 0 }
};
export const Populated: Story = {
args: { pageCount: 8 }
};
export const Selected: Story = {
args: { pageCount: 4 }
};
@@ -0,0 +1,142 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { applicationConfig, Meta, StoryObj, moduleMetadata } from '@storybook/angular';
import { PdfViewerComponent } from './pdf-viewer.component';
import { provideStoryCore } from '../../../stories/core-story.providers';
const SAMPLE_PDF_URL = 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf';
const meta: Meta<PdfViewerComponent> = {
component: PdfViewerComponent,
title: 'Core/Viewer/PDF Viewer',
decorators: [
moduleMetadata({
imports: [PdfViewerComponent]
}),
applicationConfig({
providers: [...provideStoryCore()]
})
],
parameters: {
docs: {
description: {
component: `Renders PDF documents using \`pdfjs-dist\`. Stories require the pdf.js worker assets to be served by Storybook; see the Viewer documentation for details.`
}
}
},
argTypes: {
urlFile: {
control: 'text',
description: 'External URL of the PDF file to load.',
table: { type: { summary: 'string' } }
},
blobFile: {
control: false,
description: 'In-memory Blob containing the PDF binary content.',
table: { type: { summary: 'Blob' } }
},
fileName: {
control: 'text',
description: 'Optional file name displayed by the toolbar.',
table: { type: { summary: 'string' } }
},
showToolbar: {
control: 'boolean',
description: 'Whether to show the PDF viewer toolbar (page navigation, zoom, ...).',
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } }
},
allowThumbnails: {
control: 'boolean',
description: 'Whether the thumbnails side panel can be opened.',
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
},
cacheType: {
control: 'text',
description: 'Optional `Cache-Control` header value used when loading via URL.',
table: { type: { summary: 'string' } }
},
rendered: {
action: 'rendered',
description: 'Emitted after a page is rendered.',
table: { type: { summary: 'EventEmitter <void>' }, category: 'Actions' }
},
error: {
action: 'error',
description: 'Emitted when the PDF fails to load.',
table: { type: { summary: 'EventEmitter <void>' }, category: 'Actions' }
},
close: {
action: 'close',
description: 'Emitted when the password dialog is dismissed without supplying a password.',
table: { type: { summary: 'EventEmitter <void>' }, category: 'Actions' }
},
pagesLoaded: {
action: 'pagesLoaded',
description: 'Emitted when all pages of the document finish loading.',
table: { type: { summary: 'EventEmitter <void>' }, category: 'Actions' }
}
},
args: {
urlFile: SAMPLE_PDF_URL,
fileName: 'sample.pdf',
showToolbar: true,
allowThumbnails: false
},
render: (args) => ({
props: args,
template: `<div style="height:600px; border:1px solid #ddd;">
<adf-pdf-viewer
[urlFile]="urlFile"
[fileName]="fileName"
[showToolbar]="showToolbar"
[allowThumbnails]="allowThumbnails"
[cacheType]="cacheType"
(rendered)="rendered($event)"
(error)="error($event)"
(close)="close($event)"
(pagesLoaded)="pagesLoaded($event)" />
</div>`
})
};
export default meta;
type Story = StoryObj<PdfViewerComponent>;
export const WithSamplePdf: Story = {
args: {
urlFile: SAMPLE_PDF_URL,
showToolbar: true,
allowThumbnails: false
}
};
export const WithThumbnailsEnabled: Story = {
args: {
urlFile: SAMPLE_PDF_URL,
showToolbar: true,
allowThumbnails: true
}
};
export const ToolbarHidden: Story = {
args: {
urlFile: SAMPLE_PDF_URL,
showToolbar: false,
allowThumbnails: false
}
};