Skip to content

Commit

Permalink
feat: add <PrismicImage> (#140)
Browse files Browse the repository at this point in the history
* feat: add `<PrismicImage>`

* test: check that Imgix parameters are supported in `<PrismicImage>`

* fix: only allow alt and fallbackAlt to declare an image as decorative

* fix: incorrect import

* docs: add TSDoc

* docs: document default alt behavior

* docs: add note about empty image field alt behavior
  • Loading branch information
angeloashmore authored Apr 15, 2022
1 parent bc7ad19 commit 7a93f69
Show file tree
Hide file tree
Showing 8 changed files with 596 additions and 31 deletions.
28 changes: 28 additions & 0 deletions messages/alt-must-be-an-empty-string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# `alt` must be an empty string

`<PrismicImage>` allows the the `alt` HTML attribute to be configured using two props: `alt` and `fallbackAlt`.

Both `alt` and `fallbackAlt` can only be used to [declare an image as decorative][mdn-alt-decorative-image] by pasing an empty string. You may not pass arbitrary alternative text to the `alt` prop.

```tsx
// Will render `alt` using the Image field's `alt` property.
<PrismicImage field={doc.data.imageField} />

// Will always render `alt=""`.
<PrismicImage field={doc.data.imageField} alt="" />

// Will render `alt=""` only if the Image field's alt property is empty.
<PrismicImage field={doc.data.imageField} fallbackAlt="" />
```

All images should have an alt value. `<PrismicImage>` will automatically use the Image field's `alt` property written in the Prismic Editor. If the Image field's `alt` property is empty, the `alt` HTML attribute will not be included _unless_ one of `alt` or `fallbackAlt` is used.

For more details on decorative images, [see the MDN article on the `<img>` HTMl element's `alt` attribute][mdn-alt-decorative-image].

## Deciding between `alt=""` and `fallbackAlt=""`

`alt=""` will always mark the image as decorative, ignoring the provied Image field's `alt` property. Use this when the image is always used for decorative or presentational purposes.

`fallbackAlt=""` will only mark the image as decorative if the provided Image field's `alt` property is empty. Use this when you want to mark the image as decorative _unless_ alternative text is provided in the Prismic Editor. **Generally speaking, this is discouraged**; prefer marking the image as decorative intentionally.

[mdn-alt-decorative-image]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt#decorative_images
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@
"unit": "nyc --reporter=lcovonly --reporter=text --exclude-after-remap=false ava"
},
"dependencies": {
"@prismicio/helpers": "^2.2.1",
"@prismicio/helpers": "^2.3.0",
"@prismicio/richtext": "^2.0.1"
},
"devDependencies": {
"@prismicio/client": "^6.4.2",
"@prismicio/mock": "^0.0.9",
"@prismicio/mock": "^0.0.10",
"@prismicio/types": "^0.1.27",
"@size-limit/preset-small-lib": "^7.0.8",
"@testing-library/react": "^12.1.4",
Expand Down
185 changes: 185 additions & 0 deletions src/PrismicImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import * as React from "react";
import * as prismicT from "@prismicio/types";
import * as prismicH from "@prismicio/helpers";

import { __PRODUCTION__ } from "./lib/__PRODUCTION__";
import { devMsg } from "./lib/devMsg";

/**
* Props for `<PrismicImage>`.
*/
export type PrismicImageProps = Omit<
React.DetailedHTMLProps<
React.ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
>,
"src" | "srcset" | "alt"
> & {
/**
* The Prismic Image field or thumbnail to render.
*/
field: prismicT.ImageFieldImage | null | undefined;

/**
* An object of Imgix URL API parameters to transform the image.
*
* See: https://docs.imgix.com/apis/rendering
*/
imgixParams?: Parameters<typeof prismicH.asImageSrc>[1];

/**
* Declare an image as decorative by providing `alt=""`.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt#decorative_images
*/
alt?: "";

/**
* Declare an image as decorative only if the Image field does not have
* alternative text by providing `fallbackAlt=""`.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt#decorative_images
*/
fallbackAlt?: "";
} & (
| {
/**
* Widths used to build a `srcset` value for the Image field.
*
* If a `widths` prop is not given or `"defaults"` is passed, the
* following widths will be used: 640, 750, 828, 1080, 1200, 1920, 2048, 3840.
*
* If the Image field contains responsive views, each responsive view
* can be used as a width in the resulting `srcset` by passing
* `"thumbnails"` as the `widths` prop.
*/
widths?:
| NonNullable<
Parameters<typeof prismicH.asImageWidthSrcSet>[1]
>["widths"]
| "defaults";
/**
* Not used when the `widths` prop is used.
*/
pixelDensities?: never;
}
| {
/**
* Not used when the `widths` prop is used.
*/
widths?: never;
/**
* Pixel densities used to build a `srcset` value for the Image field.
*
* If a `pixelDensities` prop is passed `"defaults"`, the following
* pixel densities will be used: 1, 2, 3.
*/
pixelDensities:
| NonNullable<
Parameters<typeof prismicH.asImagePixelDensitySrcSet>[1]
>["pixelDensities"]
| "defaults";
}
);

const _PrismicImage = (
props: PrismicImageProps,
ref: React.ForwardedRef<HTMLImageElement>,
): JSX.Element | null => {
const {
field,
alt,
fallbackAlt,
imgixParams,
widths,
pixelDensities,
...restProps
} = props;

if (!__PRODUCTION__) {
if (typeof alt === "string" && props.alt !== "") {
console.warn(
`[PrismicImage] The alt prop can only be used to declare an image as decorative by passing an empty string (alt=""). For more details, see ${devMsg(
"alt-must-be-an-empty-string",
)}`,
);
}

if (typeof fallbackAlt === "string" && fallbackAlt !== "") {
console.warn(
`[PrismicImage] The fallbackAlt prop can only be used to declare an image as decorative by passing an empty string (fallbackAlt=""). For more details, see ${devMsg(
"alt-must-be-an-empty-string",
)}`,
);
}

if (widths && pixelDensities) {
console.warn(
`[PrismicImage] Only one of "widths" or "pixelDensities" props can be provided. "widths" will be used in this case.`,
);
}
}

if (prismicH.isFilled.imageThumbnail(field)) {
let src: string | undefined;
let srcSet: string | undefined;

if (widths || !pixelDensities) {
const res = prismicH.asImageWidthSrcSet(field, {
...imgixParams,
widths: widths === "defaults" ? undefined : widths,
});

src = res.src;
srcSet = res.srcset;
} else if (pixelDensities) {
const res = prismicH.asImagePixelDensitySrcSet(field, {
...imgixParams,
pixelDensities:
pixelDensities === "defaults" ? undefined : pixelDensities,
});

src = res.src;
srcSet = res.srcset;
}

return (
<img
ref={ref}
src={src}
srcSet={srcSet}
alt={alt ?? (field.alt || fallbackAlt)}
{...restProps}
/>
);
} else {
return null;
}
};

if (!__PRODUCTION__) {
_PrismicImage.displayName = "PrismicImage";
}

/**
* React component that renders an image from a Prismic Image field or one of
* its thumbnails. It will automatically set the `alt` attribute using the Image
* field's `alt` property.
*
* By default, a widths-based srcset will be used to support responsive images.
* This ensures only the smallest image needed for a browser is downloaded.
*
* To use a pixel-density-based srcset, use the `pixelDensities` prop. Default
* pixel densities can be used by using `pixelDensities="defaults"`.
*
* **Note**: If you are using a framework that has a native image component,
* such as Next.js and Gatsby, prefer using those image components instead. They
* can provide deeper framework integration than `<PrismicImage>`.
*
* @param props - Props for the component.
*
* @returns A responsive image component for the given Image field.
*/
export const PrismicImage = React.forwardRef(_PrismicImage);
35 changes: 20 additions & 15 deletions src/PrismicLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react";
import * as prismicH from "@prismicio/helpers";
import * as prismicT from "@prismicio/types";

import { __PRODUCTION__ } from "./lib/__PRODUCTION__";
import { isInternalURL } from "./lib/isInternalURL";

import { usePrismicContext } from "./usePrismicContext";
Expand Down Expand Up @@ -118,21 +119,6 @@ const defaultInternalComponent = "a";
*/
const defaultExternalComponent = "a";

/**
* React component that renders a link from a Prismic Link field.
*
* Different components can be rendered depending on whether the link is
* internal or external. This is helpful when integrating with client-side
* routers, such as a router-specific Link component.
*
* If a link is configured to open in a new window using `target="_blank"`,
* `rel="noopener noreferrer"` is set by default.
*
* @param props - Props for the component.
*
* @returns The internal or external link component depending on whether the
* link is internal or external.
*/
const _PrismicLink = <
InternalComponent extends React.ElementType<LinkProps>,
ExternalComponent extends React.ElementType<LinkProps>,
Expand Down Expand Up @@ -215,6 +201,25 @@ const _PrismicLink = <
) : null;
};

if (!__PRODUCTION__) {
_PrismicLink.displayName = "PrismicLink";
}

/**
* React component that renders a link from a Prismic Link field.
*
* Different components can be rendered depending on whether the link is
* internal or external. This is helpful when integrating with client-side
* routers, such as a router-specific Link component.
*
* If a link is configured to open in a new window using `target="_blank"`,
* `rel="noopener noreferrer"` is set by default.
*
* @param props - Props for the component.
*
* @returns The internal or external link component depending on whether the
* link is internal or external.
*/
export const PrismicLink = React.forwardRef(_PrismicLink) as <
InternalComponent extends React.ElementType<LinkProps>,
ExternalComponent extends React.ElementType<LinkProps>,
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export { Element };
// TODO: Remove in v3.
export const Elements = Element;

export { PrismicImage } from "./PrismicImage";
export type { PrismicImageProps } from "./PrismicImage";

export { SliceZone, TODOSliceComponent } from "./SliceZone";
export type {
SliceComponentProps,
Expand Down
20 changes: 20 additions & 0 deletions src/lib/devMsg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { version } from "../../package.json";

/**
* Returns a `prismic.dev/msg` URL for a given message slug.
*
* @example
*
* ```ts
* devMsg("missing-param");
* // => "https://prismic.dev/msg/react/v1.2.3/missing-param.md"
* ```
*
* @param slug - Slug for the message. This corresponds to a Markdown file in
* the Git repository's `/messages` directory.
*
* @returns The `prismic.dev/msg` URL for the given slug.
*/
export const devMsg = (slug: string) => {
return `https://prismic.dev/msg/react/v${version}/${slug}`;
};
Loading

0 comments on commit 7a93f69

Please sign in to comment.