@@ -20,12 +20,29 @@ export type ContentMediaProps = {
2020 label ?: string ;
2121 /** Caption text shown below the media (description / source). */
2222 caption ?: ReactNode ;
23- /** Text of the optional link inside the figcaption. */
24- captionLinkLabel ?: ReactNode ;
25- /** Props of the optional link inside the figcaption. */
26- captionLinkProps ?: RegisteredLinkProps ;
23+ /**
24+ * Label and props of the optional link inside the figcaption.
25+ * Both properties are required together so the link always has an accessible
26+ * name (RGAA 6.1 / WCAG 2.4.4 – Link Purpose).
27+ */
28+ captionLink ?: {
29+ label : ReactNode ;
30+ linkProps : RegisteredLinkProps ;
31+ } ;
32+ /**
33+ * Size variant of the media container.
34+ * - `"sm"` → `fr-content-media--sm`
35+ * - `"lg"` → `fr-content-media--lg`
36+ * - `"md"` (default) → no modifier class
37+ */
38+ size ?: "sm" | "md" | "lg" ;
39+ /**
40+ * Aspect-ratio utility class applied to the image/SVG wrapper (`fr-ratio-*`).
41+ * Only applies to `type="img"` and `type="svg"`.
42+ * Example values: `"16x9"`, `"4x3"`, `"1x1"`, `"3x2"`, `"3x4"`, `"2x3"`.
43+ */
44+ ratio ?: "16x9" | "3x2" | "4x3" | "1x1" | "3x4" | "2x3" ;
2745} & ContentMediaProps . Media ;
28-
2946export namespace ContentMediaProps {
3047 /** Image (`<img>`) media. */
3148 export type ImageMedia = {
@@ -41,7 +58,6 @@ export namespace ContentMediaProps {
4158 alt : string ;
4259 } ;
4360 } ;
44-
4561 /** SVG media — pass your `<svg>` element as the `svg` prop. */
4662 export type SvgMedia = {
4763 type : "svg" ;
@@ -52,7 +68,6 @@ export namespace ContentMediaProps {
5268 */
5369 svg : ReactNode ;
5470 } ;
55-
5671 /** Embedded video via `<iframe>` (e.g. YouTube). */
5772 export type IframeMedia = {
5873 type : "iframe" ;
@@ -66,7 +81,6 @@ export namespace ContentMediaProps {
6681 title : string ;
6782 } ;
6883 } ;
69-
7084 /** Native HTML5 `<video>`. */
7185 export type VideoMedia = {
7286 type : "video" ;
@@ -78,7 +92,6 @@ export namespace ContentMediaProps {
7892 /** Props forwarded to the `<video>` element. `controls` is always set. */
7993 videoProps : React . VideoHTMLAttributes < HTMLVideoElement > & { src : string } ;
8094 } ;
81-
8295 /** Native HTML5 `<audio>`. */
8396 export type AudioMedia = {
8497 type : "audio" ;
@@ -90,10 +103,8 @@ export namespace ContentMediaProps {
90103 /** Props forwarded to the `<audio>` element. `controls` is always set. */
91104 audioProps : React . AudioHTMLAttributes < HTMLAudioElement > & { src : string } ;
92105 } ;
93-
94106 export type Media = ImageMedia | SvgMedia | IframeMedia | VideoMedia | AudioMedia ;
95107}
96-
97108/** @see <https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/contenu-medias> */
98109export const ContentMedia = memo (
99110 forwardRef < HTMLElement , ContentMediaProps > ( ( props , ref ) => {
@@ -104,32 +115,37 @@ export const ContentMedia = memo(
104115 classes = { } ,
105116 label,
106117 caption,
107- captionLinkLabel,
108- captionLinkProps
118+ captionLink,
119+ size,
120+ ratio
109121 } = props ;
110-
111122 const id = useAnalyticsId ( {
112123 "defaultIdPrefix" : "fr-content-media" ,
113124 "explicitlyProvidedId" : id_props
114125 } ) ;
115-
116126 const { Link } = getLink ( ) ;
117-
118127 const ariaLabel = label ?? ( typeof caption === "string" ? caption : undefined ) ;
119-
128+ const imgWrapperClassName = cx (
129+ fr . cx ( "fr-content-media__img" ) ,
130+ ratio !== undefined ? ( `fr-ratio-${ ratio } ` as string ) : undefined ,
131+ classes . img
132+ ) ;
120133 const renderMedia = ( ) => {
121134 switch ( props . type ) {
122135 case "img" :
123136 return (
124- < div className = { cx ( fr . cx ( "fr-content-media__img" ) , classes . img ) } >
137+ < div className = { imgWrapperClassName } >
125138 < img
126139 { ...props . imgProps }
127140 className = { cx ( fr . cx ( "fr-responsive-img" ) , props . imgProps . className ) }
128141 />
129142 </ div >
130143 ) ;
131144 case "svg" :
132- return < > { props . svg } </ > ;
145+ // The DSFR reference implementation wraps SVGs in fr-content-media__img
146+ // (example/component/content/index.html). The wrapper provides width:100%
147+ // and serves as an anchor for the fr-ratio-* size utilities.
148+ return < div className = { imgWrapperClassName } > { props . svg } </ div > ;
133149 case "iframe" :
134150 return (
135151 < iframe
@@ -159,14 +175,19 @@ export const ContentMedia = memo(
159175 ) ;
160176 }
161177 } ;
162-
163- const hasCaption = caption !== undefined || captionLinkProps !== undefined ;
164-
178+ const hasCaption = caption !== undefined || captionLink !== undefined ;
165179 return (
166180 < figure
167181 id = { id }
168182 role = "group"
169- className = { cx ( fr . cx ( "fr-content-media" ) , classes . root , className ) }
183+ className = { cx (
184+ fr . cx ( "fr-content-media" ) ,
185+ size !== undefined && size !== "md"
186+ ? ( `fr-content-media--${ size } ` as string )
187+ : undefined ,
188+ classes . root ,
189+ className
190+ ) }
170191 style = { style }
171192 { ...( ariaLabel !== undefined ? { "aria-label" : ariaLabel } : { } ) }
172193 ref = { ref }
@@ -175,9 +196,9 @@ export const ContentMedia = memo(
175196 { hasCaption && (
176197 < figcaption className = { cx ( fr . cx ( "fr-content-media__caption" ) , classes . caption ) } >
177198 { caption }
178- { captionLinkProps !== undefined && (
179- < Link { ...captionLinkProps } className = { fr . cx ( "fr-link" ) } >
180- { captionLinkLabel }
199+ { captionLink !== undefined && (
200+ < Link { ...captionLink . linkProps } className = { fr . cx ( "fr-link" ) } >
201+ { captionLink . label }
181202 </ Link >
182203 ) }
183204 </ figcaption >
@@ -186,7 +207,5 @@ export const ContentMedia = memo(
186207 ) ;
187208 } )
188209) ;
189-
190210ContentMedia . displayName = symToStr ( { ContentMedia } ) ;
191-
192211export default ContentMedia ;
0 commit comments