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
\n
type RowProps = {
\n
cache: Cache;
\n
images: string[];
\n
};
\n
\n
function RowComponent({
\n
cache,
\n
index,
\n
images,
\n
style
\n
}: RowComponentProps<RowProps>) {
\n
const url = images[index];
\n
\n
const isCached = !!cache.getImageSize(index);
\n
\n
return (
\n
<div className=\"overflow-hidden\" style={style}>
\n
{isCached || <LoadingSpinner />}
\n
<img
\n
className={isCached ? undefined : \"opacity-0\"}
\n
onLoad={(event) => {
\n
cache.setImageSize(index, {
\n
height: event.currentTarget.naturalHeight,
\n
width: event.currentTarget.naturalWidth
\n
});
\n
}}
\n
src={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\";
\n
import { List } from \"react-window\";
\n
\n
function Example({ images }: { images: string[] }) {
\n
const [listSize, setListSize] = useState<Size>({ height: 0, width: 0 });
\n
\n
// Rows will lazily register image sizes in this cache
\n
const cache = useImageSizeCache();
\n
\n
const rowHeight = useCallback(
\n
(index: number) => {
\n
const size = cache.getImageSize(index) ?? cache.getAverageSize();
\n
\n
// Scale the image to fit within the list
\n
return listSize.width * (size.height / size.width);
\n
},
\n
[cache, listSize]
\n
);
\n
\n
return (
\n
<List<RowProps>
\n
onResize={setListSize}
\n
rowComponent={RowComponent}
\n
rowCount={images.length}
\n
rowHeight={rowHeight}
\n
rowProps={{ 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 = {
\n
height: number;
\n
width: number;
\n
};
\n
\n
type Cache = {
\n
getAverageSize(): Size;
\n
getImageSize(index: number): Size | undefined;
\n
setImageSize(index: number, size: Size): void;
\n
};
\n
\n
function useImageSizeCache(): Cache {
\n
const [cachedRowHeights, setCachedRowHeights] = useState<Map<number, Size>>(
\n
new Map()
\n
);
\n
\n
const getAverageSize = useCallback(() => {
\n
let totalHeight = 0;
\n
let totalWidth = 0;
\n
\n
cachedRowHeights.forEach((size) => {
\n
totalHeight += size.height;
\n
totalWidth += size.width;
\n
});
\n
\n
if (totalHeight === 0) {
\n
return {
\n
height: 1,
\n
width: 1
\n
};
\n
}
\n
\n
return {
\n
height: totalHeight / cachedRowHeights.size,
\n
width: totalWidth / cachedRowHeights.size
\n
};
\n
}, [cachedRowHeights]);
\n
\n
const getImageSize = useCallback(
\n
(index: number) => cachedRowHeights.get(index),
\n
[cachedRowHeights]
\n
);
\n
\n
const setImageSize = useCallback((index: number, size: Size) => {
\n
setCachedRowHeights((prevMap) => {
\n
const clonedMap = new Map(prevMap);
\n
clonedMap.set(index, size);
\n
return clonedMap;
\n
});
\n
}, []);
\n
\n
return useMemo(
\n
() => ({
\n
getAverageSize,
\n
getImageSize,
\n
setImageSize
\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>( + new Map() + ); + + const getAverageSize = useCallback(() => { + let totalHeight = 0; + let totalWidth = 0; + + cachedRowHeights.forEach((size) => { + totalHeight += size.height; + totalWidth += size.width; + }); + + if (totalHeight === 0) { + return { + height: 1, + width: 1 + }; + } + + return { + height: totalHeight / cachedRowHeights.size, + width: totalWidth / cachedRowHeights.size + }; + }, [cachedRowHeights]); + + const getImageSize = useCallback( + (index: number) => cachedRowHeights.get(index), + [cachedRowHeights] + ); + + const setImageSize = useCallback((index: number, size: Size) => { + setCachedRowHeights((prevMap) => { + const clonedMap = new Map(prevMap); + clonedMap.set(index, size); + return clonedMap; + }); + }, []); + + return useMemo( + () => ({ + getAverageSize, + getImageSize, + setImageSize + }), + [getAverageSize, getImageSize, setImageSize] + ); +} + +// + +export { useImageSizeCache }; +export type { Cache, Size };