diff --git a/src/ContentMedia.tsx b/src/ContentMedia.tsx new file mode 100644 index 000000000..1be45dd22 --- /dev/null +++ b/src/ContentMedia.tsx @@ -0,0 +1,211 @@ +"use client"; + +import React, { memo, forwardRef, type CSSProperties, type ReactNode } from "react"; +import { symToStr } from "tsafe/symToStr"; +import { fr } from "./fr"; +import { cx } from "./tools/cx"; +import { getLink, type RegisteredLinkProps } from "./link"; +import { useAnalyticsId } from "./tools/useAnalyticsId"; + +export type ContentMediaProps = { + id?: string; + className?: string; + style?: CSSProperties; + classes?: Partial>; + /** + * aria-label for the `
` element. + * Defaults to the `caption` prop when it is a plain string. + * Required for image and SVG media types when caption is not a string. + */ + label?: string; + /** Caption text shown below the media (description / source). */ + caption?: ReactNode; + /** + * Label and props of the optional link inside the figcaption. + * Both properties are required together so the link always has an accessible + * name (RGAA 6.1 / WCAG 2.4.4 – Link Purpose). + */ + captionLink?: { + label: ReactNode; + linkProps: RegisteredLinkProps; + }; + /** + * Size variant of the media container. + * - `"sm"` → `fr-content-media--sm` + * - `"lg"` → `fr-content-media--lg` + * - `"md"` (default) → no modifier class + */ + size?: "sm" | "md" | "lg"; + /** + * Aspect-ratio utility class applied to the image/SVG wrapper (`fr-ratio-*`). + * Only applies to `type="img"` and `type="svg"`. + * Example values: `"16x9"`, `"4x3"`, `"1x1"`, `"3x2"`, `"3x4"`, `"2x3"`. + */ + ratio?: "16x9" | "3x2" | "4x3" | "1x1" | "3x4" | "2x3"; +} & ContentMediaProps.Media; +export namespace ContentMediaProps { + /** Image (``) media. */ + export type ImageMedia = { + type: "img"; + /** + * Props forwarded to the `` element. + * `src` and `alt` are required — `alt` can be an empty string when + * the image is purely decorative. + */ + imgProps: React.ImgHTMLAttributes & { + src: string; + /** Empty string is allowed for decorative images. */ + alt: string; + }; + }; + /** SVG media — pass your `` element as the `svg` prop. */ + export type SvgMedia = { + type: "svg"; + /** + * The `` element to render. + * • Decorative SVGs should carry `aria-hidden="true"`. + * • Meaningful SVGs should carry `role="img"` and an `aria-label`. + */ + svg: ReactNode; + }; + /** Embedded video via `

` within the `

` within the `

+ +
+ ); + case "svg": + // The DSFR reference implementation wraps SVGs in fr-content-media__img + // (example/component/content/index.html). The wrapper provides width:100% + // and serves as an anchor for the fr-ratio-* size utilities. + return
{props.svg}
; + case "iframe": + return ( +