我希望 maxHeight 函数在浏览器调整大小时执行

问题描述 投票:0回答:1

我编写了一个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

reactjs
1个回答
0
投票

安装组件时,您获得最高的组件并将所有组件的高度设置为最高。 然后当窗口调整大小时,您再次检查最高的组件并执行相同的操作。 但此时,所有组件的高度都已设置为之前的值,因此不会再次更改。 我所做的是清除调整大小事件的 maxHeight 。 然后设置一个单独的 useEffect 在 maxHeight 被清除时运行。 这样,当您检查最高的元素时,将计算您的元素高度,而无需手动高度。

const clearMaxHeight = () => { setMaxHeight(null); };
useEffect(() => {
    if (maxHeight == null) {
        calculateMaxHeight();
    }
}, [maxHeight]);

useEffect(() => {
    window.addEventListener('resize', clearMaxHeight);

    return () => { window.removeEventListener('resize', clearMaxHeight); };
}, []);
© www.soinside.com 2019 - 2024. All rights reserved.