AAE-43707 Add storybook stories for txt img and media viewers

Adds Storybook coverage for the basic content sub-viewers used by the
viewer-render renderer - txt-viewer, img-viewer and media-player -
with sample blob and URL inputs and meaningful named variants.
This commit is contained in:
Wojciech Duda
2026-05-04 17:19:40 +02:00
parent c2b63e7dfa
commit 8d07c647b7
3 changed files with 406 additions and 0 deletions
@@ -0,0 +1,151 @@
/*!
* @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 { ImgViewerComponent } from './img-viewer.component';
import { provideStoryCore } from '../../../stories/core-story.providers';
const SAMPLE_IMAGE_URL =
'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/640px-PNG_transparency_demonstration_1.png';
const LARGE_IMAGE_URL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/2560px-Cat03.jpg';
const meta: Meta<ImgViewerComponent> = {
component: ImgViewerComponent,
title: 'Core/Viewer/Image Viewer',
decorators: [
moduleMetadata({
imports: [ImgViewerComponent]
}),
applicationConfig({
providers: [...provideStoryCore()]
})
],
parameters: {
docs: {
description: {
component: `Renders an image with zoom, rotation and (optional) crop / save editing actions powered by Cropper.js.`
}
}
},
argTypes: {
urlFile: {
control: 'text',
description: 'External URL of the image to display.',
table: { type: { summary: 'string' } }
},
blobFile: {
control: false,
description: 'In-memory Blob containing the image content.',
table: { type: { summary: 'Blob' } }
},
fileName: {
control: 'text',
description: 'Optional file name used by the toolbar/aria labels.',
table: { type: { summary: 'string' } }
},
showToolbar: {
control: 'boolean',
description: 'Whether to show the image viewer toolbar (zoom, rotate, ...).',
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } }
},
readOnly: {
control: 'boolean',
description: 'When `true`, editing actions (rotate, crop, save) are hidden.',
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } }
},
allowedEditActions: {
control: 'object',
description: 'Map controlling which editing actions are enabled. Example: `{ rotate: true, crop: false }`.',
table: { type: { summary: '{ [key: string]: boolean }' } }
},
error: {
action: 'error',
description: 'Emitted when the image fails to load.',
table: { type: { summary: 'EventEmitter <void>' }, category: 'Actions' }
},
submit: {
action: 'submit',
description: 'Emitted with the edited Blob when the user saves the cropped/rotated image.',
table: { type: { summary: 'EventEmitter <Blob>' }, category: 'Actions' }
},
isSaving: {
action: 'isSaving',
description: 'Emits `true` while the edited image is being saved.',
table: { type: { summary: 'EventEmitter <boolean>' }, category: 'Actions' }
},
imageLoaded: {
action: 'imageLoaded',
description: 'Emitted once the underlying `<img>` element finishes loading.',
table: { type: { summary: 'EventEmitter <void>' }, category: 'Actions' }
}
},
args: {
urlFile: SAMPLE_IMAGE_URL,
fileName: 'sample-image.png',
showToolbar: true,
readOnly: true,
allowedEditActions: { rotate: true, crop: true }
},
render: (args) => ({
props: args,
template: `<div style="height:480px; border:1px solid #ddd;">
<adf-img-viewer
[urlFile]="urlFile"
[fileName]="fileName"
[showToolbar]="showToolbar"
[readOnly]="readOnly"
[allowedEditActions]="allowedEditActions"
(error)="error($event)"
(submit)="submit($event)"
(isSaving)="isSaving($event)"
(imageLoaded)="imageLoaded($event)" />
</div>`
})
};
export default meta;
type Story = StoryObj<ImgViewerComponent>;
export const SimpleImage: Story = {
args: {
urlFile: SAMPLE_IMAGE_URL,
readOnly: true
}
};
export const LargeImage: Story = {
args: {
urlFile: LARGE_IMAGE_URL,
readOnly: true
}
};
export const EditableWithRotateOnly: Story = {
args: {
urlFile: SAMPLE_IMAGE_URL,
readOnly: false,
allowedEditActions: { rotate: true, crop: false }
}
};
export const FullyEditable: Story = {
args: {
urlFile: SAMPLE_IMAGE_URL,
readOnly: false,
allowedEditActions: { rotate: true, crop: true }
}
};
@@ -0,0 +1,135 @@
/*!
* @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 { MediaPlayerComponent } from './media-player.component';
import { Track } from '../../models/viewer.model';
import { provideStoryCore } from '../../../stories/core-story.providers';
const SAMPLE_VIDEO_URL = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
const SAMPLE_AUDIO_URL = 'https://upload.wikimedia.org/wikipedia/commons/4/4f/Mozart_Eine_Kleine_Nachtmusik_K525_1st_movement.ogg';
const SAMPLE_TRACK: Track = {
src: 'data:text/vtt;base64,V0VCVlRUCgowMDowMDowMC4wMDAgLS0+IDAwOjAwOjAyLjAwMApIZWxsbyBmcm9tIHN0b3J5Ym9vayB0cmFja3M=',
label: 'English',
kind: 'subtitles',
srclang: 'en'
};
const meta: Meta<MediaPlayerComponent> = {
component: MediaPlayerComponent,
title: 'Core/Viewer/Media Player',
decorators: [
moduleMetadata({
imports: [MediaPlayerComponent]
}),
applicationConfig({
providers: [...provideStoryCore()]
})
],
parameters: {
docs: {
description: {
component: `Plays audio and video files using native browser controls. Accepts URLs, blobs and an optional list of subtitle tracks.`
}
}
},
argTypes: {
urlFile: {
control: 'text',
description: 'External URL of the audio/video file to play.',
table: { type: { summary: 'string' } }
},
blobFile: {
control: false,
description: 'In-memory Blob containing media data.',
table: { type: { summary: 'Blob' } }
},
mimeType: {
control: 'text',
description: 'MIME type used to determine whether to render an audio or video player.',
table: { type: { summary: 'string' } }
},
fileName: {
control: 'text',
description: 'Optional file name (used as a fallback when the MIME type cannot be derived).',
table: { type: { summary: 'string' } }
},
tracks: {
control: 'object',
description: 'Array of subtitle / caption tracks attached to the player.',
table: { type: { summary: 'Track[]' }, defaultValue: { summary: '[]' } }
},
error: {
action: 'error',
description: 'Emitted when the underlying media element raises an error.',
table: { type: { summary: 'EventEmitter <Event>' }, category: 'Actions' }
},
canPlay: {
action: 'canPlay',
description: 'Emitted when the media element reports that it can start playback.',
table: { type: { summary: 'EventEmitter <void>' }, category: 'Actions' }
}
},
args: {
urlFile: SAMPLE_VIDEO_URL,
mimeType: 'video/mp4',
fileName: 'sample.mp4',
tracks: []
},
render: (args) => ({
props: args,
template: `<div style="max-width:720px; border:1px solid #ddd;">
<adf-media-player
[urlFile]="urlFile"
[mimeType]="mimeType"
[fileName]="fileName"
[tracks]="tracks"
(error)="error($event)"
(canPlay)="canPlay($event)" />
</div>`
})
};
export default meta;
type Story = StoryObj<MediaPlayerComponent>;
export const Video: Story = {
args: {
urlFile: SAMPLE_VIDEO_URL,
mimeType: 'video/mp4',
fileName: 'sample.mp4'
}
};
export const VideoWithSubtitleTrack: Story = {
args: {
urlFile: SAMPLE_VIDEO_URL,
mimeType: 'video/mp4',
fileName: 'sample.mp4',
tracks: [SAMPLE_TRACK]
}
};
export const Audio: Story = {
args: {
urlFile: SAMPLE_AUDIO_URL,
mimeType: 'audio/ogg',
fileName: 'sample.ogg',
tracks: []
}
};
@@ -0,0 +1,120 @@
/*!
* @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 { provideHttpClient } from '@angular/common/http';
import { TxtViewerComponent } from './txt-viewer.component';
import { provideStoryCore } from '../../../stories/core-story.providers';
type TxtViewerStoryArgs = TxtViewerComponent & {
sampleText?: string;
};
const SHORT_TEXT = `Hello from the ADF Text Viewer!
Use this story to preview short snippets of plain-text content.`;
const LONG_TEXT = Array.from(
{ length: 60 },
(_, i) => `${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`
).join('\n');
const buildBlob = (content: string) => new Blob([content], { type: 'text/plain' });
const meta: Meta<TxtViewerStoryArgs> = {
component: TxtViewerComponent,
title: 'Core/Viewer/Txt Viewer',
decorators: [
moduleMetadata({
imports: [TxtViewerComponent]
}),
applicationConfig({
providers: [provideHttpClient(), ...provideStoryCore()]
})
],
parameters: {
docs: {
description: {
component: `Renders the textual content of a plain-text file from either an external URL or an in-memory \`Blob\`.`
}
}
},
argTypes: {
urlFile: {
control: 'text',
description: 'External URL pointing to the text file to load.',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'undefined' }
}
},
blobFile: {
control: false,
description: 'In-memory Blob containing the text content.',
table: {
type: { summary: 'Blob' },
defaultValue: { summary: 'undefined' }
}
},
sampleText: {
control: 'text',
description: 'Sample text content used to build the Blob input for the story.',
table: { category: 'Storybook Helpers' }
},
contentLoaded: {
action: 'contentLoaded',
description: 'Emitted when the text content finishes loading.',
table: {
type: { summary: 'EventEmitter <void>' },
category: 'Actions'
}
}
},
args: {
sampleText: SHORT_TEXT
},
render: (args) => ({
props: {
...args,
blobFile: args.sampleText ? buildBlob(args.sampleText) : undefined
},
template: `<div style="height:320px; border:1px solid #ddd; overflow:auto;">
<adf-txt-viewer [blobFile]="blobFile" (contentLoaded)="contentLoaded($event)" />
</div>`
})
};
export default meta;
type Story = StoryObj<TxtViewerStoryArgs>;
export const ShortContent: Story = {
args: {
sampleText: SHORT_TEXT
}
};
export const LongScrollingContent: Story = {
args: {
sampleText: LONG_TEXT
}
};
export const Empty: Story = {
args: {
sampleText: ''
}
};