Skip to content

isom-1898 image gallery slideshow #1298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 67 commits into from
May 30, 2025

Conversation

adriangohjw
Copy link
Member

@adriangohjw adriangohjw commented May 7, 2025

Problem

Closes https://linear.app/ogp/issue/ISOM-1898/image-gallery-slideshow

Solution

Breaking Changes

  • Yes - this PR contains breaking changes
    • Details ...
  • No - this PR is backwards compatible

Features:

  • new image galley component
  • skipped the image preload optimization part - i feel like its not an issue at this point so decided to not overengineer update: decided to still add it otherwise navigation can feel sluggish, and not somethign im proud of. And since we rarely update components after shipping it, I rather do it well now (isom-1898 image gallery slideshow #1298 (comment))

FYI @sehyunidaaa - lemme know otherwise

  • set number of images between 2 and 30.
  • there's only design for 3 or 5 preview images in Figma. I have standardized to use the height for 3 images if there's 1-3 preview images, and the height for 5 images if there's 4-5 preview images.
    • Note: 1 preview image only happens in case someone saved an invalid schema directly via DB or from github->studio migration. it should NOT be possible on studio, but adding it to storybook nevertheless
  • i have skip the image preview in studio for now image
  • side-note: im feeling half-half about transition animation after implementing it - it feels a bit more sluggish, but maybe we just have to lower the transition? just pointing out - leave it up to you to make the final call

Before & After Screenshots

Without transition

Screen.Recording.2025-05-08.at.7.25.36.AM.mov

With transition

Screen.Recording.2025-05-08.at.7.59.29.AM.mov

Tests

Go to Studio to create a new Image Gallery block in a Content page, and add more than 5 images

  • navigate with the left/right button -> it should navigate correctly
  • go to the first image and click left button -> you should be at the last image
  • go to the last image and click right button -> you should be at the first image
  • click any image directly via preview below -> it should navigate correctly
  • at any point of time, the image shown should be in the center of the preview, unless it's the first 2 images or last 2 images

adriangohjw added 21 commits May 8, 2025 04:17
…ality; add constants for arrow SVGs and maximum preview images
…ty; simplify preview logic and enhance readability. Add unit tests for getPreviewIndices functionality.
… styles for improved accessibility and visual consistency.
…r effects and disable state for active images.
…eryClient for improved modularity and maintainability. Introduce ImageGalleryClientProps type for better prop management.
…on and button state, improving clarity and consistency in index handling.
…nd styles for image previews, improve button focus states, and refactor preview index handling for better clarity and maintainability.
… sets with captions for enhanced visual representation and testing.
…ved layout and visual separation in the image gallery.
@adriangohjw adriangohjw self-assigned this May 7, 2025
@adriangohjw adriangohjw requested a review from a team as a code owner May 7, 2025 23:30
@adriangohjw adriangohjw added the enhancement New feature or request label May 7, 2025
Copy link

linear bot commented May 7, 2025

@datadog-opengovsg
Copy link

datadog-opengovsg bot commented May 7, 2025

Datadog Report

Branch report: adriangohjw/isom-1898-image-gallery-slideshow
Commit report: 2d4996f
Test service: isomer-studio

✅ 0 Failed, 566 Passed, 46 Skipped, 1m 35.05s Total Time
⬆️ Test Sessions change in coverage: 1 increased (+0.12%)

Comment on lines +9 to +13
const SingleImageSchema = Type.Object({
src: ImageSrcSchema,
alt: AltTextSchema,
caption: Type.Optional(Type.String({ maxLength: 100 })),
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[comment, no actionable] i wonder if we should consolidate this together iwth our existing image schema to avoid an explosion of similar yet subtly different schemas

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought about it - felt a bit hard since it's aldy not standardized at this moment, from schema shape to name of fields e.g. src, imageSrc

Comment on lines +84 to +89
if (image.getAttribute("loading") === "lazy") {
image.removeAttribute("loading")
const src = image.src
image.src = ""
image.src = src
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[comment] not sure if it's possible to do, but the lazy load appears to be a function of the current index and the maxPreviewImages - should be just pass in the logic here to the lazyLoad prop on ImageClient? feels abit clearer and saves us the trouble of dom manipulation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if i understand correctly. maxPreviewImages is determined by the breakpoints and not a config, and we will only know when the component is rendering

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To double check my understanding; my proposal was to do something like

<ImageClient 
  shouldLazyLoad = {shouldLazyLoad(numberOfImages, currentIndex, maxPreviewImages)}
/>

because this is client component (use client directive at top), it seems like we'll already have access to the client sided APIs. this seems like a cleaner way (+ clearer) rather than having to do DOM manipulation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, these are two unrelated sections.

The shouldLazyLoad logic in ImageClient controls whether an image should be lazy/eager-loaded during the initial render.

In contrast, the DOM manipulation above is explicitly forcing an eager load. This change (from lazy to eager) is necessary to ensure the image loads immediately. We do that through preloadImage, which is triggered by user interactions like hovering over buttons or preview images.

return Array.from({ length: numberOfImages }, (_, i) => i)
}

const numberOfImagesBeforeCurrent = Math.floor(maxPreviewImages / 2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure i understand this line here. if i have an image array [0, 1, 2, 3, 4], and i'm at position 4, the number of images before current should be 4 isn't it? similarly if i'm at position 1, it's should be 0.

not sure i understand why it's maxPreviewImages / 2 rather than just the pos

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah this do some computes

What it does:

Creates a "sliding window" of image previews (e.g., 3 or 5 images).

How it works:

  • If we have fewer images than the window size, it shows all of them.
  • Otherwise (we have more images than the window size), it tries to center the currentIndex within this window
  • It smartly adjusts the window to ensure it doesn't go off the edges (e.g., showing negative indices or indices beyond the last image). So, if currentIndex is near the start or end, the window shifts to stay within bounds.

To your question:

You've got a good point - the name numberOfImagesBeforeCurrent is a bit confusing.

  • This line const numberOfImagesBeforeCurrent = Math.floor(maxPreviewImages / 2) isn't counting the actual images before currentIndex in the whole array. Instead, it's figuring out how many images should appear before the currentIndex within the small, visible preview window to keep the currentIndex roughly centered.
  • If maxPreviewImages is 5, it tries to put 2 images before the current one in the preview.
  • If maxPreviewImages is 3, it tries to put 1 image before the current one in the preview.

The rest of the function then uses this to position that "preview window" correctly within the full list of images, making sure it doesn't go out of bounds.

Copy link
Contributor

@seaerchin seaerchin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure i'm understanding the behaviour of the pr correctly:

we have 2 forms of images

  1. the big one shown when the user clicks in
  2. the preview we see down below

the big one is eagerly preloaded when user is +/- 1 or 2 positions away (depending on the window size)

all the images in the preview we see down below is loaded when the user hovers over the preview (and we load in window size)?

Comment on lines +84 to +89
if (image.getAttribute("loading") === "lazy") {
image.removeAttribute("loading")
const src = image.src
image.src = ""
image.src = src
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To double check my understanding; my proposal was to do something like

<ImageClient 
  shouldLazyLoad = {shouldLazyLoad(numberOfImages, currentIndex, maxPreviewImages)}
/>

because this is client component (use client directive at top), it seems like we'll already have access to the client sided APIs. this seems like a cleaner way (+ clearer) rather than having to do DOM manipulation

Comment on lines +173 to +181
const shouldPreload =
// Preload the image if it is in the preview thumbnail sequence
previewIndices.includes(index) ||
// Preload the last image if currently displaying the first image
// to ensure smooth transitioning when navigating to the last image from the first
(currentIndex === 0 && index === images.length - 1) ||
// Preload the first image if currently displaying the last image
// to ensure smooth transitioning when navigating to the first image from the last
(currentIndex === images.length - 1 && index === 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[clarification] shouldn't the preview indices for currentIndex == 0 || currentIndex = 1 alr include the first/last index?

Copy link
Member Author

@adriangohjw adriangohjw May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but in the case wheres theres many images e.g. 20, the previewIndices won't include the first/last index

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[comment] i can't help but feel like this is abit confusing and probably clearer expressed with modulo arithmetic, but given its narrow scope, i'm fine w/ just keeping this as is

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually i agree. Thought about it and decided to refactor it based on 51bdb44

its more hardcoded, but given that we are sure its gonna be 3/5, we can hardcode for the respective use case. More readable and maintanable

@adriangohjw
Copy link
Member Author

@seaerchin

we have 2 forms of images

  1. the big one shown when the user clicks in
  2. the preview we see down below

correct

the big one is eagerly preloaded when user is +/- 1 or 2 positions away (depending on the window size)
all the images in the preview we see down below is loaded when the user hovers over the preview (and we load in window size)?

correct + also as they onMouseEnter the "next"/"previous" button

@adriangohjw adriangohjw merged commit 0415687 into main May 30, 2025
18 of 20 checks passed
@adriangohjw adriangohjw deleted the adriangohjw/isom-1898-image-gallery-slideshow branch May 30, 2025 06:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants