Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: video component #1131

Merged
merged 24 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom'
2 changes: 1 addition & 1 deletion jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ const config = {
// setupFiles: [],

// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
setupFilesAfterEnv: ['./jest-setup.js'], //setupFilesAfterEnv: ['<rootDir>/jest-setup.js']

// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
Expand Down
1 change: 0 additions & 1 deletion src/Accordion/AccordionHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const AccordionHeader: FC<AccordionHeaderProps> = ({
return (
<div className='accordion-header' data-testid={testId}>
<Tag
data-bs-toggle='collapse'
aria-expanded={active ? 'true' : 'false'}
className={toggleClasses}
onClick={onToggle}
Expand Down
205 changes: 205 additions & 0 deletions src/Video/Video.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import React, { FC, useEffect, useRef } from 'react';

import { VideoPlayer } from 'bootstrap-italia';
import { Input } from '../Input/Input';
import { Accordion } from '../Accordion/Accordion';
import { AccordionItem } from '../Accordion/AccordionItem';
import { AccordionHeader } from '../Accordion/AccordionHeader';
import { AccordionBody } from '../Accordion/AccordionBody';
import { Dimmer } from '../Dimmer/Dimmer';
import { DimmerButtons } from '../Dimmer/DimmerButtons';
import { Button } from '../Button/Button';
import { FormGroup, Label } from 'reactstrap';
import { Icon } from '../Icon/Icon';

export interface VideoSource {
/** Sorgente del video */
src: string;
/** Tipo della sorgente video */
type: string;
}
export interface VideoTrackSource {
/** Tipologia di traccia audio */
kind: string;
/** Sorgente della traccia audio */
src: string;
/** Lingua della traccia audio */
srcLang: string;
/** Label della traccia audio */
label: string;
/** Setta la traccia audio come default */
isDefault?: boolean;
}

export interface VideoYouTube {
/** Url del video YouTube */
url: string;
/** Attiva o meno il disclaimer */
hasDisclaimer?: boolean;
/** Testo del disclaimer */
disclaimerText?: string;
/** Chiave del disclaimer come preferenza */
disclaimerKey?: string;
}
export interface VideoProps {
/** Id del componente Video */
id: string;
/** Sorgenti del video */
sources?: Array<VideoSource>;
/** Testo della trascrizione */
transcription?: string;
/** Label dell'accordion della trascrizione */
transcriptionLabel?: string;
/** Array delle tracce */
tracks?: Array<VideoTrackSource>;
/** Url del poster */
poster?: string;
/** Attiva o meno i controlli */
controls?: boolean;
/** Attiva o meno l'autoplay */
autoPlay?: boolean;
/** Attiva o meno il loop del video */
loop?: boolean;
/** Modalità visualizzazione fluida */
fluid?: boolean;
/** Video YouTube */
youtube?: VideoYouTube;
/** Label del pulsante di accettazione */
acceptLabel?: string;
/** Label della checkbox per ricordare la scelta di accettazione */
rememberLabel?: string;
}

export const Video: FC<VideoProps> = (props) => {
const [showTranscript, setShowTranscript] = React.useState(false);
const [showDisclaimer, setShowDisclaimer] = React.useState(false);
const [instance, setInstance] = React.useState<VideoPlayer>();
const [rememberFlag, setRememberFlag] = React.useState(false);
const [disclaimerText, setDisclaimerText] = React.useState(
`Accetta i cookie di YouTube per vedere il video. Puoi gestire le preferenze nella cookie policy.`
);
const ref = useRef<HTMLVideoElement>(null);

useEffect(() => {
const el = ref.current;
if (el && VideoPlayer && !instance) {
setInstance(new VideoPlayer(el))
}
if (props.youtube?.url) {
if (props.youtube.hasDisclaimer) {
const serviceName = props.youtube.disclaimerKey || 'youtube';
const rememberFlag = localStorage.getItem(serviceName);
if (props.youtube.disclaimerText) {
setDisclaimerText(props.youtube.disclaimerText);
}
setRememberFlag(rememberFlag == 'true');
if (rememberFlag == 'true') {
setShowDisclaimer(false);
loadYouTubeVideo(props.youtube.url);
} else {
setShowDisclaimer(true);
}
}
}

if (props.autoPlay) {
setTimeout(() => {
instance?.player?.play();
}, 1000);
}

return () => {
if (instance) {
instance.dispose();
}
};
}, [instance]);

const loadYouTubeVideo = (url: string) => {
if (instance) {
instance.setYouTubeVideo(url);
}
}

const { controls = true, autoPlay = false, loop = false, fluid = true, poster = undefined } = props;
const videoProps = { controls, autoPlay, loop, fluid, poster };

return (
<>
<div className='row dimmable'>
<video {...videoProps} ref={ref} preload='auto' className='video-js' data-bs-video={true}>
{props.sources?.map((source) => <source key={source.src} src={source.src} type={source.type} />)}
{props.tracks?.map((track) => {
const { src, kind, label, srcLang, isDefault } = track;
return (
<track
key={src}
kind={kind}
src={src}
srcLang={srcLang || ''}
label={label}
default={isDefault || false}
/>
);
})}
</video>
{props.transcription && (
<Accordion className='vjs-transcription'>
<AccordionItem>
<AccordionHeader active={showTranscript} onToggle={() => setShowTranscript((p) => !p)}>
{props.transcriptionLabel || 'Trascrizione'}
</AccordionHeader>
<AccordionBody active={showTranscript}>{props.transcription}</AccordionBody>
</AccordionItem>
</Accordion>
)}
<Dimmer
show={showDisclaimer}
className='acceptoverlay-inner'
wrapperClassName='acceptoverlay acceptoverlay-primary '
>
<div className='acceptoverlay-icon'>
<Icon icon='it-video' size='xl' className=''></Icon>
</div>
<p dangerouslySetInnerHTML={{ __html: disclaimerText }}></p>
<DimmerButtons className='bg-primary'>
<Button
onClick={() => {
if (props.youtube?.url) {
if (props.youtube.hasDisclaimer) {
const serviceName = props.youtube.disclaimerKey || 'youtube';
if (rememberFlag) {
// set cookie
localStorage.setItem(serviceName, 'true');
} else {
// reset cookie
localStorage.removeItem(serviceName);
}
loadYouTubeVideo(props.youtube.url);
}
setShowDisclaimer(false);
}
}}
color='primary'
>
{props.acceptLabel || 'Accetta'}
</Button>
<div className='d-flex align-items-center ml-2'>
<FormGroup check inline>
<Input
id={`inline-checkbox_${props.id}`}
type='checkbox'
checked={rememberFlag}
onChange={() => setRememberFlag((p) => !p)}
/>
<Label check for={`inline-checkbox_${props.id}`} defaultChecked={false} className='text-white'>
{props.rememberLabel || 'Ricorda per tutti i video'}
</Label>
</FormGroup>
</div>
</DimmerButtons>
</Dimmer>
</div>
</>
);
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export { Toolbar } from './Toolbar/Toolbar';
export { ToolbarDividerItem } from './Toolbar/ToolbarDividerItem';
export { ToolbarItem } from './Toolbar/ToolbarItem';
export { Transfer } from './Transfer/Transfer';
export { Video } from './Video/Video'

// Types
export type { AccordionProps } from './Accordion/Accordion';
Expand Down Expand Up @@ -258,6 +259,7 @@ export type { TimelineProps } from './Timeline/TimelineWrapper';
export type { ToggleProps } from './Toggle/Toggle';
export type { ToolbarProps } from './Toolbar/Toolbar';
export type { ToolbarItemBadge, ToolbarItemProps } from './Toolbar/ToolbarItem';
export type { VideoProps, VideoYouTube, VideoSource, VideoTrackSource } from "./Video/Video"

export type {
BreadcrumbItemProps,
Expand Down
146 changes: 146 additions & 0 deletions stories/Components/Video.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { Video } from '../../src';
import type { VideoTrackSource } from '../../src/Video/Video';

const meta: Meta<typeof Video> = {
title: 'Documentazione/Componenti/Video',
component: Video,
parameters: {
docs: {
canvas: { sourceState: 'none' }
}
}
};

export default meta;

type Story = StoryObj<typeof Video>;

export const Trascription: Story = {
render: () => {
const sources = [
{ src: '//vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
{ src: '//vjs.zencdn.net/v/oceans.webm', type: 'video/webm' }
];
const transcription = 'Questa è la trascrizione testuale del video';

return (
<Video
id='video-trascription'
sources={sources}
transcriptionLabel={'Mostra la trascrizione del video'}
transcription={transcription}
/>
);
}
};

export const Tracks: Story = {
render: () => {
const transcription = 'Questa è la trascrizione testuale del video';
const tracks: VideoTrackSource[] = [
{
src: 'https://italia.github.io/bootstrap-italia/docs/assets/video/subtitles-it.vtt',
kind: 'subtitles',
label: 'Italiano',
srcLang: 'it',
isDefault: true
},
{
src: 'https://italia.github.io/bootstrap-italia/docs/assets/video/subtitles-en.vtt',
kind: 'subtitles',
label: 'English',
srcLang: 'en',
isDefault: false
},
{
kind: 'captions',
src: 'https://italia.github.io/bootstrap-italia/docs/assets/video/subtitles-es.vtt',
srcLang: 'es',
label: 'Español',
isDefault: false
}
];
const sources = [
{
src: 'https://italia.github.io/bootstrap-italia/docs/assets/video/ElephantsDreamDASH/ElephantsDream.mp4.mpd',
type: 'application/dash+xml'
}
];

const poster = 'https://italia.github.io/bootstrap-italia/docs/assets/video/ElephantsDream.mp4-poster21.jpg';
return (
<Video
id='video-tracks'
sources={sources}
tracks={tracks}
poster={poster}
transcriptionLabel={'Mostra la trascrizione del video'}
transcription={transcription}
/>
);
}
};

export const AutoplayAndControls: Story = {
render: () => {
const transcription = 'Questa è la trascrizione testuale del video';
const sources = [
{ src: '//vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
{ src: '//vjs.zencdn.net/v/oceans.webm', type: 'video/webm' }
];

return (
<Video
id='video-autoplay-controls'
sources={sources}
autoPlay={true}
fluid={true}
controls={true}
loop={true}
transcriptionLabel={'Mostra la trascrizione del video'}
transcription={transcription}
/>
);
}
};

export const YouTubeVideo: Story = {
render: () => {
const transcription = 'Questa è la trascrizione testuale del video';
const ytVideo = {
url: 'https://youtu.be/_0j7ZQ67KtY',
hasDisclaimer: true,
disclaimerKey: 'youtube',
disclaimerText: `Accetta i cookie di YouTube per vedere il video. Puoi gestire le preferenze nella <a class="text-white" href='/privacy_policy'>cookie policy</a>`
};
return (
<Video
id='video-youtube'
youtube={ytVideo}
transcriptionLabel={'Mostra la trascrizione del video'}
transcription={transcription}
/>
);
}
};

export const Base: Story = {
render: () => {
const transcription = 'Questa è la trascrizione testuale del video';
const sources = [
{ src: '//vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
{ src: '//vjs.zencdn.net/v/oceans.webm', type: 'video/webm' }
];

return (
<Video
id="video-simple"
sources={sources}
transcriptionLabel={'Mostra la trascrizione del video'}
transcription={transcription}
/>
);
}
};
Loading
Loading