-
-
Notifications
You must be signed in to change notification settings - Fork 133
enhance(carousel): Close carousel on browser back navigation #2262
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
base: master
Are you sure you want to change the base?
enhance(carousel): Close carousel on browser back navigation #2262
Conversation
7607088
to
b76ced7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances the carousel modal so that opening it pushes a history state and pressing the browser back button closes the carousel instead of navigating away.
- Integrates Next.js router in
CarouselProvider
to push a#carousel
hash when opening and callrouter.back()
on close - Updates
Carousel
to re-run the overflow option viasetOptions
whenever the index or source changes - Adds
useRouter
import and adjusts effect/callback dependencies for proper behavior
Comments suppressed due to low confidence (3)
components/carousel.js:121
- [nitpick] The variable
url
is ambiguous—consider renaming it tobasePath
orpathWithoutHash
to clarify that it strips the hash fragment.
const url = router.asPath.split('#')[0]
components/carousel.js:120
- [nitpick] The new history‐push and back‐navigation behavior isn’t covered by existing tests. Add a test to simulate opening the carousel and pressing the back button to ensure it closes correctly.
const showCarousel = useCallback(({ src }) => {
components/carousel.js:123
- Pushing history state alone won't close the carousel when the user hits the back button. You should listen for popstate or Next.js router events (e.g.,
router.events.on('routeChangeStart')
) and call the modal'sclose()
callback when the URL no longer contains#carousel
.
router.push(url, url + '#carousel', { shallow: true })
Happy to have a look again based on the cursor feedback, but I can confirm that the modal does close automatically upon navigating backwards, as shown in the PR description screen recording. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Closing the carousel now always scrolls us to the top:
2025-07-24.21-37-19.mp4
Let me have a look at that |
c85d3b1
to
df72819
Compare
668ade3
to
615b2f0
Compare
Should work now, switched to checking the URL hash instead, less complicated and seems to not have the scrolling issue. Screen.Recording.2025-08-18.at.17.36.26.mov |
I don't know I had accidentally deleted this one 😅 |
615b2f0
to
9b5ee24
Compare
aaed9cd
to
61f12cc
Compare
61f12cc
to
4985b92
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It works really well! This will be a great addition to the mobile experience.
Something I don't like is the fact that on every click it generates browser history, so if I open the carousel 5 times, I would need to go back 5 times without any kind of feedback (the carousel stays closed).
My personal view is that this doesn't block your PR, it's something that I see everywhere something similar is used.
I left some nitpicks that I'd like for you to address before approving, and some comments. I didn't find something else that could block your PR, great job!
components/carousel.js
Outdated
useEffect(() => { | ||
if (typeof window === 'undefined') return | ||
|
||
if (window.location.hash === '#carousel') { | ||
const confirmedEntries = Array.from(media.current.entries()) | ||
.filter(([, entry]) => entry.confirmed) | ||
|
||
if (confirmedEntries.length > 0) { | ||
showCarousel({ src: confirmedEntries[0][0] }) | ||
} | ||
} | ||
}, [showCarousel]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems redundant. It’s very rare that it would find any confirmed entries since this runs before any media has been fully loaded; confirmMedia
already does what this should do!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought so too, but cursor bot thought otherwise, seems to be quite strict on it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What did Mr. Cursor say? It has the tendency of being unaware of context sometimes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me get rid of this and see if it re-comments, keeping cursor at bay with the idea I had initially has been quite confusing.
components/carousel.js
Outdated
const confirmedEntries = Array.from(media.current.entries()) | ||
.filter(([, entry]) => entry.confirmed) | ||
if (confirmedEntries.length >= 1) { | ||
showCarousel({ src }) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: this is duplicate code, I wonder if this check can be done directly in showCarousel
or with an helper function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let me have another look
components/modal.js
Outdated
const currentHash = window.location.hash | ||
if (currentHash) { | ||
const hasModalWithHash = modalStack.current.some( | ||
modal => modal.options?.hash && `#${modal.options.hash}` === currentHash | ||
) | ||
if (hasModalWithHash) { | ||
window.history.replaceState(window.history.state, '', window.location.pathname + window.location.search) | ||
} | ||
} | ||
|
||
while (modalStack.current.length) { | ||
getCurrentContent()?.options?.onClose?.() | ||
modalStack.current.pop() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that this preserves any other hashes (like table of contents navigation) from being wrongly removed, so it might justify O(n) checks (modalStack.current.some
).
I don't see this being that big of an impact performance-wise, and to avoid O(n) checks you would need to store the original hash in some way (like a ref), so it's ok.
components/carousel.js
Outdated
@@ -114,20 +114,39 @@ function CarouselOverflow ({ originalSrc, rel }) { | |||
export function CarouselProvider ({ children }) { | |||
const media = useRef(new Map()) | |||
const showModal = useShowModal() | |||
const [isCarouselOpen, setIsCarouselOpen] = useState(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reasonable solution to avoid duplicate modals for the way they work as of now. Maybe in the future we'd need to add deduplication to the modal system.
*edit: this conflicts with confirmMedia
automatic opening of carousel, better to use a ref for this.
0cf1812
to
2c08b92
Compare
2c08b92
to
505d661
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for fixing the duplicate carousel, it works correctly and overall you did a good job.
I didn't request changes before because I wanted to discover more about the bug with you (that I discovered was present regardless of the useEffect you were using), so I'm going to do that now. Also left a question, I'll approve after!
components/carousel.js
Outdated
@@ -137,8 +151,18 @@ export function CarouselProvider ({ children }) { | |||
if (mediaItem) { | |||
mediaItem.confirmed = true | |||
media.current.set(src, mediaItem) | |||
|
|||
if (typeof window !== 'undefined' && window.location.hash === '#carousel' && !isCarouselOpenRef.current) { | |||
if (!isOpeningCarouselRef.current) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this secondary ref needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahhh, right. All the cursor bot changes had me running in loops. Forgot that's not needed anymore. Removed. Sorry about that.
505d661
to
126a6e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved!
Now a tip for your next contributions: don't amend your commit and force push! At least after making it ready for review.
It makes the review process slower and harder than it could be, because I can't compare your changes easily and I have to do git sorcery or a full review.
Got it, didn't want to pollute with many small commit fixes. |
I haven't looked very closely at this, but I wonder if this was made more complicated by choosing to use a |
I just think that for a particularly client-side issue and I think that's what hash was intended for, to handle UI state. Also looking at the URL, |
It's mostly a:
If neither is related to the hash vs query param - great. If this is the best way to do it - that's fine too, but I will not be able to merge it until I've tried alternative approaches myself. |
126a6e5
to
b44d747
Compare
You were right, I overshot this, updated to use nextjs router instead. Mostly based off of the solution you were thinking about here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if I close (either by going back or not), then go fwd, we have an imageId
in the url that doesn't pop the modal.
I suspect the way to make this work as expected is to push
on image click and showModal
as a side effect of seeing imageId
in router.query
.
I tried calling router.back()
instead of router.replace
in onClose
, but that didn't seem to work as expected.
b44d747
to
2af1ed3
Compare
2af1ed3
to
3b575f1
Compare
I think adding router.replace is better for not polluting the history stack, so just Screen.Recording.2025-09-03.at.08.47.06.mov |
Description
Add to browser history stack when opening carouseluse URL hashes to track modal state, so that navigating back on browser should close the carousel modal, not leave the post entirely.closes #1508
Screenshots
Screen.Recording.2025-08-18.at.17.36.26.mov
(Updated screen recording)
Additional Context
Was anything unclear during your work on this PR? Anything we should definitely take a closer look at?
Checklist
Are your changes backward compatible? Please answer below:
Y
For example, a change is not backward compatible if you removed a GraphQL field or dropped a database column.
On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:
10
For frontend changes: Tested on mobile, light and dark mode? Please answer below:
Y
Did you introduce any new environment variables? If so, call them out explicitly here:
N