我编写了一个calculateMaxHeight函数,它计算网格上卡片的最大高度并将其应用于网格中的所有卡片,我希望卡片高度在屏幕尺寸变化时动态变化。我已将该函数包含在“调整大小”事件侦听器中,但卡片仍然获得在页面加载时计算的高度,并且当我更改屏幕尺寸时,高度不会调整。当我刷新页面时,高度发生变化。
我附上下面的代码
'use client'
import { useState, forwardRef } from 'react'
import Image from 'next/image'
import { motion, AnimatePresence } from 'framer-motion'
import CheckMarkIcon from '@mui/icons-material/CheckCircleOutline'
import AddIcon from '@mui/icons-material/Add'
import RemoveIcon from '@mui/icons-material/Remove'
import { Button, GradientBar } from '@/components/atoms'
import { truncateText } from '@/utilities/truncateText'
export interface Props {
icon: {
imageUrl: string
description: string
}
title: string
subTitle: string
mainText: string
cTAText: string
cTALink: string
isVertical?: boolean
featuresList: string[]
subsector?: string[]
height?: string
}
// animation variants
const listOfFeatures = {
visible: {
opacity: 1,
height: 'auto',
transition: {
when: 'beforeChildren',
staggerChildren: 0.3,
},
},
hidden: {
opacity: 0,
height: 0,
transition: {
when: 'afterChildren',
duration: 0.3,
},
},
}
const featureItem = {
visible: {
opacity: 1,
transition: { duration: 0.3 },
},
hidden: {
opacity: 0,
},
}
interface IFeaturesButtonProps {
toggle: () => void
areFeaturesVisible: boolean
}
const FeaturesButton = ({
toggle,
areFeaturesVisible,
}: IFeaturesButtonProps) => {
const [isHovered, setIsHovered] = useState(false)
return (
<button
onClick={toggle}
className="flex w-fit items-center gap-1 font-montserrat text-base font-semibold text-cobalt-500"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<span className="-mt-2 ">
{areFeaturesVisible ? (
<RemoveIcon fontSize="inherit" />
) : (
<AddIcon fontSize="inherit" />
)}
</span>
<span className="flex flex-col gap-1">
{areFeaturesVisible ? 'Hide Features' : 'Show Features'}
<GradientBar
isTransparent
isActive={isHovered}
orientation="horizontal"
/>
</span>
</button>
)
}
const ProductCard = forwardRef<HTMLDivElement, Props>(
(
{
icon,
title,
subTitle,
mainText,
cTAText,
cTALink,
isVertical = false,
featuresList = [],
height = 'auto',
},
ref,
) => {
const [areFeaturesVisible, setAreFeaturesVisible] = useState(false)
const toggleFeatureVisibility = () => {
setAreFeaturesVisible(!areFeaturesVisible)
}
const truncatedMainText = truncateText(mainText, 250)
const { imageUrl = '', description = '' } = icon || {}
return (
<div
ref={ref}
data-testid="product-card"
style={{
height: areFeaturesVisible ? 'auto' : height, // Apply height conditionally
}}
className={`grid grid-cols-1 gap-8 ${isVertical ? '' : 'sm:grid-cols-[1fr_auto_1fr]'} w-full max-w-[804px] rounded-2xl p-8 shadow-elevation16`}
>
<div className="flex h-[178px] flex-col gap-4 font-montserrat sm:h-full">
<Image src={imageUrl} width={80} height={80} alt={description} />
<div>
<h3 className="text-xl font-bold text-neutral-charcoal-base">
{title}
</h3>
<p className="text-neutral-grey-01">{subTitle}</p>
</div>
{!isVertical && (
<div className="hidden sm:block">
<Button isLink url={cTALink} state="primary" text={cTAText} />
</div>
)}
<div className={`w-full ${!isVertical ? 'sm:hidden' : ''}`}>
<GradientBar
orientation="horizontal"
isActive={areFeaturesVisible}
isTransparent={false}
/>
</div>
</div>
<div className={`hidden h-full ${!isVertical ? 'sm:block' : ''}`}>
<GradientBar
orientation="vertical"
isActive={areFeaturesVisible}
isTransparent={false}
/>
</div>
<div className="flex flex-col gap-8 font-proximanova">
<div className="flex flex-col gap-4">
<p className="break-words text-neutral-charcoal-02">
{truncatedMainText}
</p>
{featuresList.length > 0 && (
<FeaturesButton
toggle={toggleFeatureVisibility}
areFeaturesVisible={areFeaturesVisible}
/>
)}
<AnimatePresence>
{areFeaturesVisible && (
<motion.ul
initial="hidden"
animate="visible"
// exit="hidden"
variants={listOfFeatures}
className="flex flex-col gap-2.5"
>
{featuresList.map((items, index) => (
<motion.li
key={index}
className="flex items-center gap-2.5 text-base leading-5 text-neutral-charcoal-03"
variants={featureItem}
>
<CheckMarkIcon
className="text-state-success"
fontSize="medium"
/>
{items}
</motion.li>
))}
</motion.ul>
)}
</AnimatePresence>
</div>
<div className={`${!isVertical ? 'block sm:hidden' : 'mt-auto'}`}>
<Button isLink url={cTALink} state="primary" text={cTAText} />
</div>
</div>
</div>
)
},
)
ProductCard.displayName = 'ProductCard'
export default ProductCard
'use client'
import { useEffect, useId, useState, Fragment, useRef } from 'react'
import { motion, useAnimation } from 'framer-motion'
import { useInView } from 'react-intersection-observer'
import ArrowRightIcon from '@mui/icons-material/ChevronRight'
import ArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
import { truncateText } from '@/utilities/truncateText'
import { GradientBar, ProductCard } from '@/components'
import { Props as ProductCardProps } from '@/components/molecules/ProductCard/ProductCard'
import useMeasure from 'react-use-measure'
export interface IProps {
title: string
sector?: string
description: string
productCards: ProductCardProps[]
portfolio?: string
subSectors?: string[]
sectionID?: string
}
const MAX_TEXT_LENGTH = 275
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.3,
},
},
}
const item = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.3, ease: 'easeOut' },
},
}
interface ISubsectorButtonProps {
toggle: () => void
isActive: boolean
text: string
}
const SubsectorButton = ({ toggle, isActive, text }: ISubsectorButtonProps) => {
const [isHovered, setIsHovered] = useState(false)
return (
<button
onClick={toggle}
className={`p-2 font-montserrat transition-all duration-200 ${isActive ? 'font-bold text-font-base' : 'hover:text-shadow font-medium text-font-light hover:text-font-base'}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{text}
<span className="ml-3">
{isActive ? <ArrowDownIcon /> : <ArrowRightIcon />}
</span>
<span className="mt-4 block">
<GradientBar
orientation="horizontal"
isActive={isActive || (isHovered && !isActive)}
isTransparent
/>
</span>
</button>
)
}
const ProductCardGrid = ({
title,
sector = '',
description,
productCards,
portfolio = '',
subSectors = [],
sectionID,
}: IProps) => {
const componentId = useId()
const headingId = `${componentId}-heading`
const descriptionText = truncateText(description, MAX_TEXT_LENGTH)
const [filteredCards, setFilteredCards] = useState(productCards)
const [selectedSubSector, setSelectedSubSector] = useState<string>()
const controls = useAnimation()
const { ref, inView } = useInView({
trackVisibility: true,
delay: 400,
})
useEffect(() => {
if (inView) {
controls.start('visible')
}
}, [controls, inView, filteredCards])
const [maxHeight, setMaxHeight] = useState(0)
const [windowWidth, setWindowWidth] = useState(0)
const cardRefs = useRef<(HTMLElement | null)[]>([])
const calculateMaxHeight = () => {
console.log('calculateMaxHeight called')
console.log('cardRefs.current:', cardRefs.current)
const heights = cardRefs.current.map((card) => card?.offsetHeight || 0)
console.log('Heights of cards:', heights)
const calculatedMaxHeight = Math.max(...heights)
setMaxHeight(calculatedMaxHeight)
console.log('calculatedMaxHeight:', calculatedMaxHeight)
}
useEffect(() => {
const handleResize = () => {
calculateMaxHeight() // Recalculate heights on resize
}
// Initial calculation on component mount
calculateMaxHeight()
// Attach resize event listener
window.addEventListener('resize', handleResize)
// Cleanup event listener on unmount
return () => window.removeEventListener('resize', handleResize)
}, [])
//Check if atleast one product card has subsector
//'some' returns true if atleast one card has a subsector
const hasSubsectors = productCards.some(
(card) => card.subsector && card.subsector.length > 0,
)
// Extract unique subsectors from product cards
const uniqueSubSectors = Array.from(
new Set(
productCards
.flatMap((card) => card.subsector || [])
.filter((subsector) => subSectors.includes(subsector)),
),
)
// Filter product cards based on the selected subsector
useEffect(() => {
const filtered = selectedSubSector
? productCards.filter((card) =>
card.subsector?.includes(selectedSubSector),
)
: productCards
setFilteredCards(filtered)
}, [selectedSubSector, productCards])
return (
<section
id={sectionID}
aria-labelledby={headingId}
className="px-4 py-16 md:px-6 lg:p-16 xl:px-[156px]"
>
<h2
id={headingId}
className="mx-auto mb-6 text-center font-montserrat font-bold text-gray-700 xxs:leading-7 xs:text-2xl sm:text-[32px] sm:leading-10 md:w-[868px] lg:text-[40px] lg:leading-[50px]"
>
{title.split(' ').map((word, index) => (
<span key={index}>
{sector.split(' ').includes(word) ? (
<span className="text-orange-brand">{word}</span>
) : (
word
)}
{index < title.split(' ').length - 1 && ' '}
</span>
))}
</h2>
<p
data-testid="product-grid-description"
className="mx-auto mb-14 text-center font-proximanova font-normal leading-6 text-font-eclipse sm:w-[720px] sm:text-lg md:w-[868px] md:text-xl"
>
{descriptionText}
</p>
{portfolio && portfolio.length > 0 && hasSubsectors && (
<Fragment>
<p className="mb-2 text-base font-normal sm:hidden">
Filter portfolio by sub-sector
</p>
<div className="relative mb-14 flex flex-col items-center justify-center gap-8 rounded-lg border border-charcoal-900 p-4 font-proximanova text-base font-semibold sm:border-0 md:flex-row">
<select
onChange={(e) => {
const selectedValue = e.target.value
setSelectedSubSector(
selectedValue === 'all' ? '' : selectedValue,
)
}}
className="w-full bg-white p-2 font-montserrat outline-0 sm:hidden"
value={selectedSubSector}
>
<option value="all">{`Full ${portfolio} portfolio`}</option>
{uniqueSubSectors.map((subSector) => (
<option key={subSector} value={subSector}>
{subSector}
</option>
))}
</select>
<div className="hidden flex-wrap justify-center gap-6 sm:flex">
<SubsectorButton
toggle={() => setSelectedSubSector('')}
isActive={!selectedSubSector}
text={`Full ${portfolio} portfolio`}
/>
{uniqueSubSectors.map((subSector) => (
<SubsectorButton
key={subSector}
toggle={() => setSelectedSubSector(subSector)}
isActive={selectedSubSector === subSector}
text={subSector}
/>
))}
</div>
</div>
</Fragment>
)}
<div className="flex justify-center">
<div className="border-charcoal-500 inline-block rounded-[32px] border bg-charcoal-100 xxs:p-6 lg:p-8">
<motion.div
ref={ref}
className={`grid justify-center xxs:gap-6 lg:gap-8 ${
filteredCards.length === 1
? 'grid-cols-1'
: filteredCards.length === 2
? 'grid-cols-1 sm:grid-cols-2'
: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
}`}
variants={container}
initial="hidden"
animate={controls}
>
{filteredCards.map((card, index) => (
<motion.div key={`${selectedSubSector}-${index}`} variants={item}>
<ProductCard
{...card}
isVertical={filteredCards.length > 1}
ref={(el: any) => (cardRefs.current[index] = el)}
height={maxHeight ? `${maxHeight}px` : 'auto'} // Convert maxHeight to px string
/>
</motion.div>
))}
</motion.div>
</div>
</div>
</section>
)
}
export default ProductCardGrid
安装组件时,您获得最高的组件并将所有组件的高度设置为最高。 然后当窗口调整大小时,您再次检查最高的组件并执行相同的操作。 但此时,所有组件的高度都已设置为之前的值,因此不会再次更改。 我所做的是清除调整大小事件的 maxHeight 。 然后设置一个单独的 useEffect 在 maxHeight 被清除时运行。 这样,当您检查最高的元素时,将计算您的元素高度,而无需手动高度。
const clearMaxHeight = () => { setMaxHeight(null); };
useEffect(() => {
if (maxHeight == null) {
calculateMaxHeight();
}
}, [maxHeight]);
useEffect(() => {
window.addEventListener('resize', clearMaxHeight);
return () => { window.removeEventListener('resize', clearMaxHeight); };
}, []);