Skip to content

Commit

Permalink
Add figure customization options (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
mathewjordan authored Nov 21, 2024
1 parent f9884f6 commit 3b869a9
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 40 deletions.
36 changes: 29 additions & 7 deletions pages/docs/scroll.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ Provide a [IIIF Presentation API](https://iiif.io/api/presentation/3.0/) Manifes

- Renders the Canvases of a IIIF Manifest as HTML5 `article` elements
- Outputs `Annotation` textual content along with `OpenSeadragon` for images
- Automatically creates a client-side search index for full-text search of Annotations
- Supports vertical scrolling and textual discovery with a fixed **Search...** input
- Supports `commenting`, `transcribing`, and `translating` motivations

## Installation

Expand Down Expand Up @@ -104,11 +102,12 @@ const MyCustomScroll = () => {

`Scroll` can configured through an `options` prop, which will serve as a object for common options.

| Prop | Type | Required | Default |
| ---------------- | -------- | -------- | ------- |
| `iiifContent` | `string` | Yes | |
| `options` | `object` | No | |
| `options.offset` | `number` | No | `0` |
| Prop | Type | Required | Default |
| ---------------- | --------------------- | -------- | ------- |
| `iiifContent` | `string` | Yes | |
| `options` | `object` | No | |
| `options.offset` | `number` | No | `0` |
| `options.figure` | [See Figure](#figure) | No | |

### Offset

Expand All @@ -120,3 +119,26 @@ The `options.offset` refers to the number of pixels to offset the fixed **Search
options={{ offset: 90 }}
/>
```

### Figure

The Scroll component renders a `figure` element for each Canvas. The `options.figure` object allows for customization of the `figure` width, aspect ratio, and display. This can be useful for customizing the Scroll component to fit within a specific layout alongside other image viewers.

| Prop | Type | Required | Default |
| ---------------------------- | ---------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ |
| `options.figure.aspectRatio` | `number` | No | `1.618` [Golden Ratio](https://en.wikipedia.org/wiki/Golden_ratio) |
| `options.figure.display` | `thumbnail`, `image-viewer` | No | `image-viewer` |
| `options.figure.width` | `string` [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/width) | No | `38.2%` |

```jsx
<Scroll
iiifContent="https://digital.lib.utk.edu/assemble/manifest/civilwar/5390"
options={{
figure: {
aspectRatio: 1,
display: "thumbnail",
width: "200px",
},
}}
/>
```
5 changes: 4 additions & 1 deletion src/components/Scroll/Figure/Figure.styled.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { AspectRatio } from "@radix-ui/react-aspect-ratio";
import { styled } from "src/styles/stitches.config";

const StyledFigurePlaceholder = styled(AspectRatio, {});

const StyledFigure = styled("figure", {
figcaption: {
display: "flex",
Expand All @@ -16,4 +19,4 @@ const StyledFigure = styled("figure", {
},
});

export { StyledFigure };
export { StyledFigure, StyledFigurePlaceholder };
43 changes: 36 additions & 7 deletions src/components/Scroll/Figure/Figure.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import React, { useContext } from "react";
import { ScrollContext, initialState } from "src/context/scroll-context";
import {
StyledFigure,
StyledFigurePlaceholder,
} from "src/components/Scroll/Figure/Figure.styled";

import { CanvasNormalized } from "@iiif/presentation-3";
import FigureCaption from "src/components/Scroll/Figure/Caption";
import ImageViewer from "src/components/Scroll/Figure/ImageViewer";
import { ScrollContext } from "src/context/scroll-context";
import { StyledFigure } from "src/components/Scroll/Figure/Figure.styled";
import FigureImageViewer from "src/components/Scroll/Figure/ImageViewer";
import FigureThumbnail from "./Thumbnail";
import { getPaintingResource } from "src/hooks/use-iiif";

interface CanvasProps {
Expand All @@ -17,17 +21,42 @@ interface CanvasProps {

const ScrollCanvasFigure: React.FC<CanvasProps> = ({ canvas, canvasInfo }) => {
const { state } = useContext(ScrollContext);
const { vault } = state;
const { vault, options } = state;
const { figure } = options;

const display = figure.display
? figure.display
: initialState.options.figure.display;
const aspectRatio = figure.aspectRatio
? figure.aspectRatio
: initialState.options.figure.aspectRatio;

const painting = getPaintingResource(vault, canvas.id);

if (!painting) return null;

return (
<StyledFigure>
{painting?.map((body) => (
<ImageViewer body={body} key={body?.id} label={canvas?.label} />
))}
{painting?.map((body) => {
return (
<StyledFigurePlaceholder ratio={aspectRatio}>
{display === "thumbnail" && (
<FigureThumbnail
body={body}
label={canvas?.label}
key={body?.id}
/>
)}
{display === "image-viewer" && (
<FigureImageViewer
label={canvas?.label}
body={body}
key={body?.id}
/>
)}
</StyledFigurePlaceholder>
);
})}
<FigureCaption canvas={canvas} canvasInfo={canvasInfo} />
</StyledFigure>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Scroll/Figure/ImageViewer.styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { styled } from "src/styles/stitches.config";

const StyledImageViewer = styled("div", {
width: "100%",
height: "360px",
height: "100%",
background: "#6662",
backgroundSize: "contain",
color: "white",
Expand Down
55 changes: 55 additions & 0 deletions src/components/Scroll/Figure/Thumbnail.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
IIIFExternalWebResource,
InternationalString,
} from "@iiif/presentation-3";
import { render, screen } from "@testing-library/react";

import FigureThumbnail from "./Thumbnail";
import React from "react";

const body: IIIFExternalWebResource = {
id: "https://example.com/iiif/2/image/example.jpg",
type: "Image",
format: "image/jpeg",
width: 500,
height: 500,
service: [
{
id: "https://example.com/iiif/2/image",
type: "ImageService2",
profile: "level2",
},
],
};

const label: InternationalString = {
en: ["Test label"],
};

describe("<FigureThumbnail />", () => {
it("renders the thumbnail with label as alt", () => {
render(<FigureThumbnail body={body} label={label} />);

const thumbnail = screen.getByTestId("scroll-figure-thumbnail");
expect(thumbnail).toBeInTheDocument();

const img = thumbnail.querySelector("img");
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute(
"src",
"https://example.com/iiif/2/image/full/500,500/0/default.jpg",
);
expect(img).toHaveAttribute("alt", "Test label");
});

it("renders the thumbnail without a service", () => {
render(<FigureThumbnail body={{ ...body, service: undefined }} />);

const img = screen.getByRole("img");
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute(
"src",
"https://example.com/iiif/2/image/example.jpg",
);
});
});
32 changes: 32 additions & 0 deletions src/components/Scroll/Figure/Thumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
IIIFExternalWebResource,
InternationalString,
} from "@iiif/presentation-3";

import { Thumbnail as CloverPrimitivesThumbnail } from "src/components/Primitives";
import React from "react";
import { StyledImageViewer } from "src/components/Scroll/Figure/ImageViewer.styled";

interface FigureThumbnailProps {
body: IIIFExternalWebResource;
label?: InternationalString | null;
}

const FigureThumbnail: React.FC<FigureThumbnailProps> = ({ body, label }) => {
return (
<StyledImageViewer data-testid="scroll-figure-thumbnail">
<CloverPrimitivesThumbnail
thumbnail={[body]}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
objectPosition: "center",
}}
altAsLabel={label as InternationalString}
/>
</StyledImageViewer>
);
};

export default React.memo(FigureThumbnail);
14 changes: 11 additions & 3 deletions src/components/Scroll/Items/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
StyledItemFigure,
StyledItemTextualBodies,
} from "src/components/Scroll/Items/Items.styled";
import { ScrollContext, initialState } from "src/context/scroll-context";

import React from "react";
import { ScrollContext } from "src/context/scroll-context";
import ScrollFigure from "src/components/Scroll/Figure/Figure";
import ScrollItemBody from "src/components/Scroll/Annotation/Body";

Expand All @@ -33,7 +33,8 @@ const ScrollItem: React.FC<ScrollItemProps> = ({
itemNumber,
}) => {
const { state } = React.useContext(ScrollContext);
const { annotations, vault } = state;
const { annotations, vault, options } = state;
const { figure } = options;

const canvas = vault?.get(item) as CanvasNormalized;

Expand Down Expand Up @@ -67,7 +68,14 @@ const ScrollItem: React.FC<ScrollItemProps> = ({
data-page-number={itemNumber}
data-last-item={isLastItem}
>
<StyledItemFigure>
<StyledItemFigure
css={{
width: figure.width
? figure.width
: initialState.options.figure.width,
}}
data-width={figure.width}
>
{canvas && <ScrollFigure canvas={canvas} canvasInfo={canvasInfo} />}
</StyledItemFigure>
<StyledItemTextualBodies>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Scroll/Items/Items.styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ const StyledItem = styled("article", {

const StyledItemFigure = styled("div", {
transition: "$all",
width: "38.2%",
flexShrink: 0,
});

const StyledItemTextualBodies = styled("div", {
width: "61.8%",
display: "flex",
flexGrow: 1,
flexDirection: "column",
justifyContent: "flex-start",

Expand Down
34 changes: 16 additions & 18 deletions src/components/Scroll/Items/Items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,23 @@ import { StyledScrollItems } from "src/components/Scroll/Items/Items.styled";

const ScrollItems = ({ items }: { items: Reference<"Canvas">[] }) => {
return (
<>
<StyledScrollItems>
{items.map((item, index) => {
const itemNumber = index + 1;
const isLastItem = itemNumber === items.length;
<StyledScrollItems>
{items.map((item, index) => {
const itemNumber = index + 1;
const isLastItem = itemNumber === items.length;

return (
<ScrollItem
item={item}
hasItemBreak={itemNumber < items.length}
isLastItem={isLastItem}
key={item.id}
itemCount={items.length}
itemNumber={itemNumber}
/>
);
})}
</StyledScrollItems>
</>
return (
<ScrollItem
item={item}
hasItemBreak={itemNumber < items.length}
isLastItem={isLastItem}
key={item.id}
itemCount={items.length}
itemNumber={itemNumber}
/>
);
})}
</StyledScrollItems>
);
};

Expand Down
12 changes: 11 additions & 1 deletion src/context/scroll-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ interface StateType {
manifest?: ManifestNormalized;
options: {
offset: number;
figure: {
display: "thumbnail" | "image-viewer";
aspectRatio?: number;
width?: CSSStyleDeclaration["width"];
};
};
searchActiveMatch?: string;
searchMatches?: {
Expand All @@ -25,11 +30,16 @@ interface ActionType {
type: string;
}

const initialState: StateType = {
export const initialState: StateType = {
annotations: [],
manifest: undefined,
options: {
offset: 0,
figure: {
display: "image-viewer",
aspectRatio: 100 / 61.8, // golden ratio
width: "38.2%",
},
},
searchActiveMatch: undefined,
searchMatches: undefined,
Expand Down

0 comments on commit 3b869a9

Please sign in to comment.