Commit ef6d1f20 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: [DetailsV2] Show data page header when nft scrolled out of view (#6585)

* show data page header when nft scrolled out of view

* add new snapshot test

* useRef for observer

* add comment

---------
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent 10b156ff
...@@ -3,7 +3,14 @@ import { render } from 'test-utils/render' ...@@ -3,7 +3,14 @@ import { render } from 'test-utils/render'
import { DataPage } from './DataPage' import { DataPage } from './DataPage'
it('placeholder containers load', () => { it('data page loads with header showing', () => {
const { asFragment } = render(<DataPage asset={TEST_NFT_ASSET} />) const { asFragment } = render(<DataPage asset={TEST_NFT_ASSET} showDataHeader={true} />)
expect(asFragment()).toMatchSnapshot()
})
// The header is hidden via opacity: 0 to maintain its spacing, so it still exists in the DOM
// Therefore we can not check for its non-existence and instead rely on comparing the full generated snapshots
it('data page loads without header showing', () => {
const { asFragment } = render(<DataPage asset={TEST_NFT_ASSET} showDataHeader={false} />)
expect(asFragment()).toMatchSnapshot() expect(asFragment()).toMatchSnapshot()
}) })
...@@ -35,6 +35,20 @@ const DataPageContainer = styled(Column)` ...@@ -35,6 +35,20 @@ const DataPageContainer = styled(Column)`
margin: 0 auto; margin: 0 auto;
` `
const HeaderContainer = styled.div<{ showDataHeader?: boolean }>`
position: sticky;
top: ${({ theme }) => `${theme.navHeight}px`};
padding-top: 16px;
backdrop-filter: blur(12px);
z-index: 1;
transition: ${({ theme }) => `opacity ${theme.transition.duration.fast}`};
opacity: ${({ showDataHeader }) => (showDataHeader ? '1' : '0')};
@media screen and (max-width: ${BREAKPOINTS.md}px) {
display: none;
}
`
const ContentContainer = styled(Row)` const ContentContainer = styled(Row)`
gap: 24px; gap: 24px;
padding-bottom: 45px; padding-bottom: 45px;
...@@ -50,11 +64,13 @@ const LeftColumn = styled(Column)` ...@@ -50,11 +64,13 @@ const LeftColumn = styled(Column)`
align-self: flex-start; align-self: flex-start;
` `
export const DataPage = ({ asset }: { asset: GenieAsset }) => { export const DataPage = ({ asset, showDataHeader }: { asset: GenieAsset; showDataHeader: boolean }) => {
return ( return (
<DataPagePaddingContainer> <DataPagePaddingContainer>
<DataPageContainer> <DataPageContainer>
<DataPageHeader asset={asset} /> <HeaderContainer showDataHeader={showDataHeader}>
<DataPageHeader asset={asset} />
</HeaderContainer>
<ContentContainer> <ContentContainer>
<LeftColumn> <LeftColumn>
{!!asset.traits?.length && <DataPageTraits asset={asset} />} {!!asset.traits?.length && <DataPageTraits asset={asset} />}
......
...@@ -10,9 +10,6 @@ import { BREAKPOINTS, ThemedText } from 'theme' ...@@ -10,9 +10,6 @@ import { BREAKPOINTS, ThemedText } from 'theme'
const HeaderContainer = styled(Row)` const HeaderContainer = styled(Row)`
gap: 24px; gap: 24px;
@media screen and (max-width: ${BREAKPOINTS.md}px) {
display: none;
}
` `
const AssetImage = styled.img` const AssetImage = styled.img`
......
...@@ -3,10 +3,26 @@ import { render } from 'test-utils/render' ...@@ -3,10 +3,26 @@ import { render } from 'test-utils/render'
import { LandingPage } from './LandingPage' import { LandingPage } from './LandingPage'
beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn()
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
})
window.IntersectionObserver = mockIntersectionObserver
})
describe('LandingPage', () => { describe('LandingPage', () => {
const mockSetShowDataHeader = jest.fn()
it('renders it correctly', () => { it('renders it correctly', () => {
const { asFragment } = render( const { asFragment } = render(
<LandingPage asset={TEST_NFT_ASSET} collection={TEST_NFT_COLLECTION_INFO_FOR_ASSET} /> <LandingPage
asset={TEST_NFT_ASSET}
collection={TEST_NFT_COLLECTION_INFO_FOR_ASSET}
setShowDataHeader={mockSetShowDataHeader}
/>
) )
expect(asFragment()).toMatchSnapshot() expect(asFragment()).toMatchSnapshot()
}) })
......
...@@ -2,6 +2,7 @@ import Column, { ColumnCenter } from 'components/Column' ...@@ -2,6 +2,7 @@ import Column, { ColumnCenter } from 'components/Column'
import Row from 'components/Row' import Row from 'components/Row'
import { VerifiedIcon } from 'nft/components/icons' import { VerifiedIcon } from 'nft/components/icons'
import { CollectionInfoForAsset, GenieAsset } from 'nft/types' import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { useEffect, useRef } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme' import { BREAKPOINTS } from 'theme'
...@@ -102,12 +103,36 @@ const MediaContainer = styled.div` ...@@ -102,12 +103,36 @@ const MediaContainer = styled.div`
interface LandingPageProps { interface LandingPageProps {
asset: GenieAsset asset: GenieAsset
collection: CollectionInfoForAsset collection: CollectionInfoForAsset
setShowDataHeader: (showDataHeader: boolean) => void
} }
export const LandingPage = ({ asset, collection }: LandingPageProps) => { export const LandingPage = ({ asset, collection, setShowDataHeader }: LandingPageProps) => {
const intersectionRef = useRef<HTMLDivElement>(null)
const observableRef = useRef(
new IntersectionObserver((entries) => {
if (!entries[0].isIntersecting) {
setShowDataHeader(true)
} else {
setShowDataHeader(false)
}
})
)
// Checks if the intersectionRef is in the viewport
// If it is not in the viewport, the data page header becomes visible
useEffect(() => {
const cachedRef = intersectionRef.current
const observer = observableRef.current
if (cachedRef && observer) {
observer.observe(cachedRef)
return () => observer.unobserve(cachedRef)
}
return
}, [intersectionRef, observableRef, setShowDataHeader])
return ( return (
<LandingPageContainer> <LandingPageContainer>
<MediaContainer> <MediaContainer ref={intersectionRef}>
<MediaRenderer asset={asset} /> <MediaRenderer asset={asset} />
</MediaContainer> </MediaContainer>
<InfoContainer> <InfoContainer>
......
import { CollectionInfoForAsset, GenieAsset } from 'nft/types' import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { useState } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
...@@ -27,12 +28,13 @@ const DetailsContentContainer = styled.div` ...@@ -27,12 +28,13 @@ const DetailsContentContainer = styled.div`
` `
export const NftDetails = ({ asset, collection }: NftDetailsProps) => { export const NftDetails = ({ asset, collection }: NftDetailsProps) => {
const [showDataHeader, setShowDataHeader] = useState(false)
return ( return (
<> <>
{asset.imageUrl && <DetailsBackground backgroundImage={asset.imageUrl} />} {asset.imageUrl && <DetailsBackground backgroundImage={asset.imageUrl} />}
<DetailsContentContainer> <DetailsContentContainer>
<LandingPage asset={asset} collection={collection} /> <LandingPage asset={asset} collection={collection} setShowDataHeader={setShowDataHeader} />
<DataPage asset={asset} /> <DataPage asset={asset} showDataHeader={showDataHeader} />
</DetailsContentContainer> </DetailsContentContainer>
</> </>
) )
......
...@@ -257,12 +257,6 @@ exports[`Header loads with asset with a sell order 1`] = ` ...@@ -257,12 +257,6 @@ exports[`Header loads with asset with a sell order 1`] = `
border-radius: 16px; border-radius: 16px;
} }
@media screen and (max-width:768px) {
.c2 {
display: none;
}
}
@media screen and (max-width:1024px) { @media screen and (max-width:1024px) {
.c3 { .c3 {
display: none; display: none;
...@@ -561,12 +555,6 @@ exports[`Header loads with asset with no sell orders 1`] = ` ...@@ -561,12 +555,6 @@ exports[`Header loads with asset with no sell orders 1`] = `
border-radius: 16px; border-radius: 16px;
} }
@media screen and (max-width:768px) {
.c2 {
display: none;
}
}
@media screen and (max-width:1024px) { @media screen and (max-width:1024px) {
.c3 { .c3 {
display: none; display: none;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment