diff --git a/public/generated/code-snippets/ImageRow.json b/public/generated/code-snippets/ImageRow.json
new file mode 100644
index 00000000..fbc86147
--- /dev/null
+++ b/public/generated/code-snippets/ImageRow.json
@@ -0,0 +1,3 @@
+{
+ "html": "
import { type RowComponentProps } from \"react-window\";
\n
\ntype RowProps = {
\ncache: Cache;
\nimages: string[];
\n};
\n
\nfunction RowComponent({
\ncache,
\nindex,
\nimages,
\nstyle
\n}: RowComponentProps<RowProps>) {
\nconst url = images[index];
\n
\nconst isCached = !!cache.getImageSize(index);
\n
\nreturn (
\n<div className=\"overflow-hidden\" style={style}>
\n{isCached || <LoadingSpinner />}
\n<img
\nclassName={isCached ? undefined : \"opacity-0\"}
\nonLoad={(event) => {
\ncache.setImageSize(index, {
\nheight: event.currentTarget.naturalHeight,
\nwidth: event.currentTarget.naturalWidth
\n});
\n}}
\nsrc={url}
\n/>
\n</div>
\n);
\n}
"
+}
\ No newline at end of file
diff --git a/public/generated/code-snippets/Images.json b/public/generated/code-snippets/Images.json
new file mode 100644
index 00000000..61dbdcb9
--- /dev/null
+++ b/public/generated/code-snippets/Images.json
@@ -0,0 +1,3 @@
+{
+ "html": "import { useCallback, useState } from \"react\";
\nimport { List } from \"react-window\";
\n
\nfunction Example({ images }: { images: string[] }) {
\nconst [listSize, setListSize] = useState<Size>({ height: 0, width: 0 });
\n
\n
\nconst cache = useImageSizeCache();
\n
\nconst rowHeight = useCallback(
\n(index: number) => {
\nconst size = cache.getImageSize(index) ?? cache.getAverageSize();
\n
\n
\nreturn listSize.width * (size.height / size.width);
\n},
\n[cache, listSize]
\n);
\n
\nreturn (
\n<List<RowProps>
\nonResize={setListSize}
\nrowComponent={RowComponent}
\nrowCount={images.length}
\nrowHeight={rowHeight}
\nrowProps={{ cache, images }}
\n/>
\n);
\n}
"
+}
\ No newline at end of file
diff --git a/public/generated/code-snippets/useImageSizeCache.json b/public/generated/code-snippets/useImageSizeCache.json
new file mode 100644
index 00000000..3bc7b9c0
--- /dev/null
+++ b/public/generated/code-snippets/useImageSizeCache.json
@@ -0,0 +1,3 @@
+{
+ "html": "type Size = {
\nheight: number;
\nwidth: number;
\n};
\n
\ntype Cache = {
\ngetAverageSize(): Size;
\ngetImageSize(index: number): Size | undefined;
\nsetImageSize(index: number, size: Size): void;
\n};
\n
\nfunction useImageSizeCache(): Cache {
\nconst [cachedRowHeights, setCachedRowHeights] = useState<Map<number, Size>>(
\nnew Map()
\n);
\n
\nconst getAverageSize = useCallback(() => {
\nlet totalHeight = 0;
\nlet totalWidth = 0;
\n
\ncachedRowHeights.forEach((size) => {
\ntotalHeight += size.height;
\ntotalWidth += size.width;
\n});
\n
\nif (totalHeight === 0) {
\nreturn {
\nheight: 1,
\nwidth: 1
\n};
\n}
\n
\nreturn {
\nheight: totalHeight / cachedRowHeights.size,
\nwidth: totalWidth / cachedRowHeights.size
\n};
\n}, [cachedRowHeights]);
\n
\nconst getImageSize = useCallback(
\n(index: number) => cachedRowHeights.get(index),
\n[cachedRowHeights]
\n);
\n
\nconst setImageSize = useCallback((index: number, size: Size) => {
\nsetCachedRowHeights((prevMap) => {
\nconst clonedMap = new Map(prevMap);
\nclonedMap.set(index, size);
\nreturn clonedMap;
\n});
\n}, []);
\n
\nreturn useMemo(
\n() => ({
\ngetAverageSize,
\ngetImageSize,
\nsetImageSize
\n}),
\n[getAverageSize, getImageSize, setImageSize]
\n);
\n}
"
+}
\ No newline at end of file
diff --git a/public/images/animal-3546613_1280.jpg b/public/images/animal-3546613_1280.jpg
new file mode 100644
index 00000000..202ecafe
Binary files /dev/null and b/public/images/animal-3546613_1280.jpg differ
diff --git a/public/images/ball-bearings-1958083_1280.jpg b/public/images/ball-bearings-1958083_1280.jpg
new file mode 100644
index 00000000..dda18d62
Binary files /dev/null and b/public/images/ball-bearings-1958083_1280.jpg differ
diff --git a/public/images/bourke-luck-potholes-163065_1280.jpg b/public/images/bourke-luck-potholes-163065_1280.jpg
new file mode 100644
index 00000000..112c0680
Binary files /dev/null and b/public/images/bourke-luck-potholes-163065_1280.jpg differ
diff --git a/public/images/child-1439468_1280.jpg b/public/images/child-1439468_1280.jpg
new file mode 100644
index 00000000..bc331f04
Binary files /dev/null and b/public/images/child-1439468_1280.jpg differ
diff --git a/public/images/digiart-3405596_1280.jpg b/public/images/digiart-3405596_1280.jpg
new file mode 100644
index 00000000..ab0b2e1b
Binary files /dev/null and b/public/images/digiart-3405596_1280.jpg differ
diff --git a/public/images/electrical-cable-mess-2654084_1280.jpg b/public/images/electrical-cable-mess-2654084_1280.jpg
new file mode 100644
index 00000000..b9cff0bc
Binary files /dev/null and b/public/images/electrical-cable-mess-2654084_1280.jpg differ
diff --git a/public/images/elephant-8608983_1280.jpg b/public/images/elephant-8608983_1280.jpg
new file mode 100644
index 00000000..3a72277e
Binary files /dev/null and b/public/images/elephant-8608983_1280.jpg differ
diff --git a/public/images/fema-4987740_1280.jpg b/public/images/fema-4987740_1280.jpg
new file mode 100644
index 00000000..629dd022
Binary files /dev/null and b/public/images/fema-4987740_1280.jpg differ
diff --git a/public/images/log-3135150_1280.jpg b/public/images/log-3135150_1280.jpg
new file mode 100644
index 00000000..4af002c4
Binary files /dev/null and b/public/images/log-3135150_1280.jpg differ
diff --git a/public/images/man-1838330_1280.jpg b/public/images/man-1838330_1280.jpg
new file mode 100644
index 00000000..47443f3f
Binary files /dev/null and b/public/images/man-1838330_1280.jpg differ
diff --git a/public/images/manipulation-2735724_1280.jpg b/public/images/manipulation-2735724_1280.jpg
new file mode 100644
index 00000000..3e1cfc1b
Binary files /dev/null and b/public/images/manipulation-2735724_1280.jpg differ
diff --git a/public/images/newborn-6467761_1280.jpg b/public/images/newborn-6467761_1280.jpg
new file mode 100644
index 00000000..c8fe7bc0
Binary files /dev/null and b/public/images/newborn-6467761_1280.jpg differ
diff --git a/public/images/old-farm-house-2096642_1280.jpg b/public/images/old-farm-house-2096642_1280.jpg
new file mode 100644
index 00000000..9eed0c49
Binary files /dev/null and b/public/images/old-farm-house-2096642_1280.jpg differ
diff --git a/public/images/people-2557534_1280.jpg b/public/images/people-2557534_1280.jpg
new file mode 100644
index 00000000..e5176144
Binary files /dev/null and b/public/images/people-2557534_1280.jpg differ
diff --git a/public/images/photo-1516712109157-6a67f5d73fa1.jpg b/public/images/photo-1516712109157-6a67f5d73fa1.jpg
new file mode 100644
index 00000000..68b5def7
Binary files /dev/null and b/public/images/photo-1516712109157-6a67f5d73fa1.jpg differ
diff --git a/public/images/photo-1562123408-fbf8cbf92c03.jpg b/public/images/photo-1562123408-fbf8cbf92c03.jpg
new file mode 100644
index 00000000..42ca9b73
Binary files /dev/null and b/public/images/photo-1562123408-fbf8cbf92c03.jpg differ
diff --git a/public/images/sculpture-99484_1280.jpg b/public/images/sculpture-99484_1280.jpg
new file mode 100644
index 00000000..78a645f0
Binary files /dev/null and b/public/images/sculpture-99484_1280.jpg differ
diff --git a/public/images/sport-4765008_1280.jpg b/public/images/sport-4765008_1280.jpg
new file mode 100644
index 00000000..1e3c701a
Binary files /dev/null and b/public/images/sport-4765008_1280.jpg differ
diff --git a/public/images/styrofoam-19493_1280.jpg b/public/images/styrofoam-19493_1280.jpg
new file mode 100644
index 00000000..328eb4f9
Binary files /dev/null and b/public/images/styrofoam-19493_1280.jpg differ
diff --git a/public/images/trabi-328402_1280.jpg b/public/images/trabi-328402_1280.jpg
new file mode 100644
index 00000000..4cd024c3
Binary files /dev/null and b/public/images/trabi-328402_1280.jpg differ
diff --git a/public/images/trailers-5073244_1280.jpg b/public/images/trailers-5073244_1280.jpg
new file mode 100644
index 00000000..9abdcb29
Binary files /dev/null and b/public/images/trailers-5073244_1280.jpg differ
diff --git a/public/images/tub-114349_1280.jpg b/public/images/tub-114349_1280.jpg
new file mode 100644
index 00000000..c34f782e
Binary files /dev/null and b/public/images/tub-114349_1280.jpg differ
diff --git a/public/images/venus-fly-trap-3684935_1280.jpg b/public/images/venus-fly-trap-3684935_1280.jpg
new file mode 100644
index 00000000..74ce68ed
Binary files /dev/null and b/public/images/venus-fly-trap-3684935_1280.jpg differ
diff --git a/public/images/web-5013633_1280.jpg b/public/images/web-5013633_1280.jpg
new file mode 100644
index 00000000..2a26c0aa
Binary files /dev/null and b/public/images/web-5013633_1280.jpg differ
diff --git a/public/images/winter-1675197_1280.jpg b/public/images/winter-1675197_1280.jpg
new file mode 100644
index 00000000..51dbe576
Binary files /dev/null and b/public/images/winter-1675197_1280.jpg differ
diff --git a/public/images/woman-1838149_1280.jpg b/public/images/woman-1838149_1280.jpg
new file mode 100644
index 00000000..ccd74184
Binary files /dev/null and b/public/images/woman-1838149_1280.jpg differ
diff --git a/src/nav/Nav.tsx b/src/nav/Nav.tsx
index 38b45002..bbf1f6c5 100644
--- a/src/nav/Nav.tsx
+++ b/src/nav/Nav.tsx
@@ -25,6 +25,7 @@ export function Nav() {
Right to left content
Horizontal lists
+ Variable size images
Sticky rows
diff --git a/src/routes.ts b/src/routes.ts
index 04019983..c43a2539 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -25,6 +25,7 @@ export const routes = {
() => import("./routes/tables/AriaRolesRoute")
),
"/list/sticky-rows": lazy(() => import("./routes/list/StickyRowsRoute")),
+ "/list/images": lazy(() => import("./routes/list/ImagesRoute")),
// SimpleGrid
"/grid/grid": lazy(() => import("./routes/grid/RenderingGridRoute")),
diff --git a/src/routes/grid/HorizontalListsRoute.tsx b/src/routes/grid/HorizontalListsRoute.tsx
index 3546c338..887ef9a0 100644
--- a/src/routes/grid/HorizontalListsRoute.tsx
+++ b/src/routes/grid/HorizontalListsRoute.tsx
@@ -3,6 +3,7 @@ import HorizontalListCellRendererMarkdown from "../../../public/generated/code-s
import { Block } from "../../components/Block";
import { Box } from "../../components/Box";
import { Code } from "../../components/code/Code";
+import { Header } from "../../components/Header";
import { LoadingSpinner } from "../../components/LoadingSpinner";
import { HorizontalList } from "./examples/HorizontalList.example";
import { useEmails } from "./hooks/useEmails";
@@ -12,6 +13,7 @@ export default function HorizontalListsRoute() {
return (
+
A horizontal list is just a grid with only one row.
Here's an example horizontal list (grid) of emails:
diff --git a/src/routes/grid/RTLGridsRoute.tsx b/src/routes/grid/RTLGridsRoute.tsx
index 20b54167..8d39f4cb 100644
--- a/src/routes/grid/RTLGridsRoute.tsx
+++ b/src/routes/grid/RTLGridsRoute.tsx
@@ -3,6 +3,7 @@ import { Block } from "../../components/Block";
import { Box } from "../../components/Box";
import { Code } from "../../components/code/Code";
import { ExternalLink } from "../../components/ExternalLink";
+import { Header } from "../../components/Header";
import { LoadingSpinner } from "../../components/LoadingSpinner";
import { RtlExample } from "./examples/RtlGrid.example";
import { useContacts } from "./hooks/useContacts";
@@ -12,6 +13,7 @@ export default function RTLGridsRoute() {
return (
+
Grids can also display right to left languages (like Arabic). The grid
components check the{" "}
diff --git a/src/routes/list/ImagesRoute.tsx b/src/routes/list/ImagesRoute.tsx
new file mode 100644
index 00000000..beafb856
--- /dev/null
+++ b/src/routes/list/ImagesRoute.tsx
@@ -0,0 +1,35 @@
+import { Block } from "../../components/Block";
+import { Box } from "../../components/Box";
+import { Code } from "../../components/code/Code";
+import { Header } from "../../components/Header";
+import { ExampleWithImages } from "./examples/Images.example";
+import ImageRowMarkdown from "../../../public/generated/code-snippets/ImageRow.json";
+import ImagesMarkdown from "../../../public/generated/code-snippets/Images.json";
+import useImageSizeCacheMarkdown from "../../../public/generated/code-snippets/useImageSizeCache.json";
+
+export default function ImagesRoute() {
+ return (
+
+
+
+ Lists can be used to render content of unknown sizes, though it requires
+ a user-provided cache.
+
+ Here's an example of a list of images of varying sizes:
+
+
+
+
+ First, let's look at the custom cache we'll use to store image sizes.
+
+
+
+ When a row is rendered for the first time, it should record the image
+ size in the cache.
+
+
+ The list can use the cache to determine the size of each row.
+
+
+ );
+}
diff --git a/src/routes/list/examples/ImageRow.example.tsx b/src/routes/list/examples/ImageRow.example.tsx
new file mode 100644
index 00000000..ac59b1ea
--- /dev/null
+++ b/src/routes/list/examples/ImageRow.example.tsx
@@ -0,0 +1,56 @@
+import { Box } from "../../../components/Box";
+import type { Cache } from "./useImageSizeCache.example";
+
+//
+
+import { type RowComponentProps } from "react-window";
+
+type RowProps = {
+ cache: Cache;
+ images: string[];
+};
+
+function RowComponent({
+ cache,
+ index,
+ images,
+ style
+}: RowComponentProps) {
+ const url = images[index];
+
+ const isCached = !!cache.getImageSize(index);
+
+ return (
+
+ {isCached ||
}
+
![]()
{
+ cache.setImageSize(index, {
+ height: event.currentTarget.naturalHeight,
+ width: event.currentTarget.naturalWidth
+ });
+ }}
+ src={url}
+ />
+
+ );
+}
+
+//
+
+function LoadingSpinner() {
+ return (
+
+ Loading...
+
+ );
+}
+
+export { RowComponent };
+export type { RowProps };
diff --git a/src/routes/list/examples/Images.example.tsx b/src/routes/list/examples/Images.example.tsx
new file mode 100644
index 00000000..38065cf9
--- /dev/null
+++ b/src/routes/list/examples/Images.example.tsx
@@ -0,0 +1,73 @@
+import { RowComponent, type RowProps } from "./ImageRow.example";
+import type { Size } from "./useImageSizeCache.example";
+import { useImageSizeCache } from "./useImageSizeCache.example";
+
+//
+
+import { useCallback, useState } from "react";
+import { List } from "react-window";
+
+function Example({ images }: { images: string[] }) {
+ const [listSize, setListSize] = useState({ height: 0, width: 0 });
+
+ // Rows will lazily register image sizes in this cache
+ const cache = useImageSizeCache();
+
+ const rowHeight = useCallback(
+ (index: number) => {
+ const size = cache.getImageSize(index) ?? cache.getAverageSize();
+
+ // Scale the image to fit within the list
+ return listSize.width * (size.height / size.width);
+ },
+ [cache, listSize]
+ );
+
+ return (
+
+ onResize={setListSize}
+ rowComponent={RowComponent}
+ rowCount={images.length}
+ rowHeight={rowHeight}
+ rowProps={{ cache, images }}
+ />
+ );
+}
+
+//
+
+const IMAGES: string[] = [
+ "/images/animal-3546613_1280.jpg",
+ "/images/ball-bearings-1958083_1280.jpg",
+ "/images/bourke-luck-potholes-163065_1280.jpg",
+ "/images/child-1439468_1280.jpg",
+ "/images/digiart-3405596_1280.jpg",
+ "/images/electrical-cable-mess-2654084_1280.jpg",
+ "/images/elephant-8608983_1280.jpg",
+ "/images/fema-4987740_1280.jpg",
+ "/images/log-3135150_1280.jpg",
+ "/images/man-1838330_1280.jpg",
+ "/images/manipulation-2735724_1280.jpg",
+ "/images/newborn-6467761_1280.jpg",
+ "/images/old-farm-house-2096642_1280.jpg",
+ "/images/people-2557534_1280.jpg",
+ "/images/photo-1516712109157-6a67f5d73fa1.jpg",
+ "/images/photo-1562123408-fbf8cbf92c03.jpg",
+ "/images/sculpture-99484_1280.jpg",
+ "/images/sport-4765008_1280.jpg",
+ "/images/styrofoam-19493_1280.jpg",
+ "/images/trabi-328402_1280.jpg",
+ "/images/trailers-5073244_1280.jpg",
+ "/images/tub-114349_1280.jpg",
+ "/images/venus-fly-trap-3684935_1280.jpg",
+ "/images/web-5013633_1280.jpg",
+ "/images/winter-1675197_1280.jpg",
+ "/images/woman-1838149_1280.jpg"
+];
+
+function ExampleWithImages() {
+ return ;
+}
+
+export { ExampleWithImages, RowComponent };
+export type { Size };
diff --git a/src/routes/list/examples/useImageSizeCache.example.ts b/src/routes/list/examples/useImageSizeCache.example.ts
new file mode 100644
index 00000000..1dda94c9
--- /dev/null
+++ b/src/routes/list/examples/useImageSizeCache.example.ts
@@ -0,0 +1,69 @@
+import { useCallback, useMemo, useState } from "react";
+
+//
+
+type Size = {
+ height: number;
+ width: number;
+};
+
+type Cache = {
+ getAverageSize(): Size;
+ getImageSize(index: number): Size | undefined;
+ setImageSize(index: number, size: Size): void;
+};
+
+function useImageSizeCache(): Cache {
+ const [cachedRowHeights, setCachedRowHeights] = useState