Maison  >  Questions et réponses  >  le corps du texte

Le parent accordéon imbriqué React ne met pas à jour la hauteur

J'essaie de créer une version mobile de ma page d'accueil et il semble y avoir un bug avec mes "éléments" en accordéon imbriqués où ils n'affichent pas la hauteur correcte de la section inférieure des éléments lors de la première ouverture.

Pour l'ouvrir, vous cliquez d'abord sur le texte du projet, puis il répertorie les projets, puis cliquez sur le projet pour basculer la fiche du projet.

(Mise à jour) Je pense que cela se produit parce que l'accordéon de mon parent ne met pas à jour sa hauteur lorsque l'accordéon de l'enfant est ouvert.

Connaissez-vous une bonne façon de procéder ? Ou si nécessaire, dois-je restructurer mes composants de manière à ce que cela soit possible ? La difficulté, c'est que l'Accordéon accepte les enfants, et je réutilise l'Accordéon à l'intérieur, donc c'est assez déroutant. Je sais que je peux utiliser une fonction de rappel pour déclencher le parent, mais je ne sais pas trop comment procéder.

Accueil.tsx

import { Accordion } from "@/components/atoms/Accordion"
import { AccordionGroup } from "@/components/atoms/AccordionGroup"
import { AccordionSlideOut } from "@/components/atoms/AccordionSlideOut"
import { Blog } from "@/components/compositions/Blog"
import { Contact } from "@/components/compositions/Contact"
import { Portfolio } from "@/components/compositions/Portfolio"
import { PuyanWei } from "@/components/compositions/PuyanWei"
import { Resumé } from "@/components/compositions/Resumé"
import { Socials } from "@/components/compositions/Socials"
import { Component } from "@/shared/types"

interface HomepageProps extends Component {}

export function Homepage({ className = "", testId = "homepage" }: HomepageProps) {
  return (
    <main className={`grid grid-cols-12 pt-24 ${className}`} data-testid={testId}>
      <section className="col-span-10 col-start-2">
        <AccordionGroup>
          <Accordion title="Puyan Wei">
            <PuyanWei />
          </Accordion>
          <Accordion className="lg:hidden" title="Portfolio">
            <Portfolio />
          </Accordion>
          <AccordionSlideOut className="hidden lg:flex" title="Portfolio">
            <Portfolio />
          </AccordionSlideOut>
          <Accordion title="Resumé">
            <Resumé />
          </Accordion>
          <Accordion title="Contact">
            <Contact />
          </Accordion>
          <Accordion title="Blog">
            <Blog />
          </Accordion>
          <Accordion title="Socials">
            <Socials />
          </Accordion>
        </AccordionGroup>
      </section>
    </main>
  )
}

portfolio.tsx

import { Accordion } from "@/components/atoms/Accordion"
import { AccordionGroup } from "@/components/atoms/AccordionGroup"
import { ProjectCard } from "@/components/molecules/ProjectCard"
import { projects } from "@/shared/consts"
import { Component } from "@/shared/types"

interface PortfolioProps extends Component {}

export function Portfolio({ className = "", testId = "portfolio" }: PortfolioProps) {
  return (
    <AccordionGroup className={`overflow-hidden ${className}`} testId={testId}>
      {projects.map((project, index) => (
        <Accordion title={project.title} key={`${index}-${project}`} headingSize="h2">
          <ProjectCard project={project} />
        </Accordion>
      ))}
    </AccordionGroup>
  )
}

AccordionGroup.tsx - Le but d'un AccordionGroup est de permettre à un seul Accordéon enfant d'être ouvert à la fois. Si un Accordion ne fait pas partie d'un AccordionGroup, il peut être ouvert et fermé indépendamment.

"use client"

import React, { Children, ReactElement, cloneElement, isValidElement, useState } from "react"
import { AccordionProps } from "@/components/atoms/Accordion"
import { Component } from "@/shared/types"

interface AccordionGroupProps extends Component {
  children: ReactElement<AccordionProps>[]
}

export function AccordionGroup({
  children,
  className = "",
  testId = "accordion-group",
}: AccordionGroupProps) {
  const [activeAccordion, setActiveAccordion] = useState<number | null>(null)

  function handleAccordionToggle(index: number) {
    setActiveAccordion((prevIndex) => (prevIndex === index ? null : index))
  }

  return (
    <div className={className} data-testid={testId}>
      {Children.map(children, (child, index) =>
        isValidElement(child)
          ? cloneElement(child, {
              onClick: () => handleAccordionToggle(index),
              isActive: activeAccordion === index,
              children: child.props.children,
              title: child.props.title,
            })
          : child
      )}
    </div>
  )
}

accordéon.tsx

"use client"
import { Component } from "@/shared/types"
import React, { MutableRefObject, ReactNode, RefObject, useEffect, useRef, useState } from "react"
import { Heading } from "@/components/atoms/Heading"

export interface AccordionProps extends Component {
  title: string
  children: ReactNode
  isActive?: boolean
  onClick?: () => void
  headingSize?: "h1" | "h2"
}

export function Accordion({
  className = "",
  title,
  children,
  isActive,
  onClick,
  headingSize = "h1",
  testId = "Accordion",
}: AccordionProps) {
  const [isOpen, setIsOpen] = useState(false)
  const [height, setHeight] = useState("0px")
  const contentHeight = useRef(null) as MutableRefObject<HTMLElement | null>

  useEffect(() => {
    if (isActive === undefined) return
    isActive ? setHeight(`${contentHeight.current?.scrollHeight}px`) : setHeight("0px")
  }, [isActive])

  function handleToggle() {
    if (!contentHeight?.current) return
    setIsOpen((prevState) => !prevState)
    setHeight(isOpen ? "0px" : `${contentHeight.current.scrollHeight}px`)
    if (onClick) onClick()
  }
  return (
    <div className={`w-full text-lg font-medium text-left focus:outline-none ${className}`}>
      <button onClick={handleToggle} data-testid={testId}>
        <Heading
          className="flex items-center justify-between"
          color={isActive ? "text-blue-200" : "text-white"}
          level={headingSize}
        >
          {title}
        </Heading>
      </button>
      <div
        className={`overflow-hidden transition-max-height duration-250 ease-in-out`}
        ref={contentHeight as RefObject<HTMLDivElement>}
        style={{ maxHeight: height }}
      >
        <div className="pt-2 pb-4">{children}</div>
      </div>
    </div>
  )
}

ProjectCard.tsx

import Image from "next/image"
import { Card } from "@/components/atoms/Card"
import { Children, Component, Project } from "@/shared/types"
import { Subheading } from "@/components/atoms/Subheading"
import { Tag } from "@/components/atoms/Tag"
import { Text } from "@/components/atoms/Text"

interface ProjectCardProps extends Component {
  project: Project
}

export function ProjectCard({
  className = "",
  testId = "project-card",
  project,
}: ProjectCardProps) {
  const {
    title,
    description,
    coverImage: { src, alt, height, width },
    tags,
  } = project
  return (
    <Card className={`flex min-h-[300px] ${className}`} data-testid={testId}>
      <div className="w-1/2">
        <CoverImage className="relative w-full h-full mb-4 -mx-6-mt-6">
          <Image
            className="absolute inset-0 object-cover object-center w-full h-full rounded-l-md"
            src={src}
            alt={alt}
            width={parseInt(width)}
            height={parseInt(height)}
            loading="eager"
          />
        </CoverImage>
      </div>
      <div className="w-1/2 p-4 px-8 text-left">
        <Subheading className="text-3xl font-bold" color="text-black">
          {title}
        </Subheading>
        <Tags className="flex flex-wrap pb-2">
          {tags.map((tag, index) => (
            <Tag className="mt-2 mr-2" key={`${index}-${tag}`} text={tag} />
          ))}
        </Tags>
        <Text color="text-black" className="text-sm">
          {description}
        </Text>
      </div>
    </Card>
  )
}

function CoverImage({ children, className }: Children) {
  return <div className={className}>{children}</div>
}
function Tags({ children, className }: Children) {
  return <div className={className}>{children}</div>
}

Toute aide serait grandement appréciée, merci !

P粉293341969P粉293341969263 Il y a quelques jours326

répondre à tous(2)je répondrai

  • P粉998100648

    P粉9981006482024-02-22 10:41:59

    Analyse du problème :

    TL;DR : L'accordéon parent doit être informé de ces changements afin de pouvoir ajuster sa hauteur en conséquence.

    Je pense que vous utilisez peut-être amiut/accordionify comme indiqué via "Création d'accordéons React légers" de Amin A. Rezapour.
    C'est le seul projet que j'ai trouvé qui utilise AccordionGroup.

    La structure en accordéon imbriquée dans l'application implique une relation parent-enfant, dans laquelle la hauteur de l'accordéon enfant change de manière dynamique selon qu'il est agrandi ou réduit.

    Cela peut être illustré par votre Portfolio.tsx, où le composant Portfolio.tsx 来说明,其中 AccordionGroup 组件包含多个基于 创建的 Accordion 组件项目 数组。这些 Accordion contient plusieurs tableaux Accordion éléments de composant créés sur la base de . Ces composants Accordéon sont les accordéons "enfants" mentionnés :

    export function Portfolio({ className = "", testId = "portfolio" }: PortfolioProps) {
      return (
        <AccordionGroup className={`overflow-hidden ${className}`} testId={testId}>
          {projects.map((project, index) => (
            <Accordion title={project.title} key={`${index}-${project}`} headingSize="h2">
              <ProjectCard project={project} />
            </Accordion>
          ))}
        </AccordionGroup>
      )
    }
    

    Chaque sousAccordion 都包含一个显示项目详细信息的ProjectCard。当用户单击Accordion(或“项目”)时,它会展开以显示ProjectCard.
    C'est là que les changements de hauteur entrent en jeu ; l'accordéon se dilate ou s'effondre en fonction de l'interaction de l'utilisateur, modifiant ainsi sa hauteur de manière dynamique.

    Les hauteurs dynamiques sont gérées en Accordion.tsx :

      function handleToggle() {
        if (!contentHeight?.current) return
        setIsOpen((prevState) => !prevState)
        setHeight(isOpen ? "0px" : `${contentHeight.current.scrollHeight}px`)
        if (onClick) onClick()
      }
    

    quand on l'appellehandleToggle函数时,它会检查手风琴当前是否打开(isOpen). Si c'est le cas, la hauteur est définie sur "0px" (c'est-à-dire l'accordéon replié). S'il n'est pas ouvert, la hauteur est définie sur la hauteur de défilement du contenu (c'est-à-dire que l'accordéon est développé).

    Les changements dynamiques de hauteur de ces accordéons pour enfants sont un élément clé de votre problème. L'accordéon parent doit être informé de ces changements afin de pouvoir ajuster sa hauteur en conséquence.

    J'ai vu dans le même Accordion.tsx :

      useEffect(() => {
        if (isActive === undefined) return
        isActive ? setHeight(`${contentHeight.current?.scrollHeight}px`) : setHeight("0px")
      }, [isActive])
    

    La hauteur de l'accordéon est basée sur isActive 属性设置的,它表示手风琴当前是否打开。如果打开,则高度设置为手风琴内容的滚动高度(有效展开手风琴),如果未激活,则高度设置为 0px (accordéon pliant).

    Cependant, bien que cet effet ajuste correctement la hauteur de chaque accordéon en fonction de son propre état isActive, il ne prend pas en compte les changements de hauteur des accordéons des enfants.

    Lorsqu'un accordéon emboîté (enfant) change de hauteur (en raison d'une expansion ou d'un effondrement), la hauteur de l'accordéon parent n'est pas recalculée et ne s'ajuste donc pas pour s'adapter à la nouvelle taille des enfants du parent.

    En d'autres termes, lorsque la hauteur de l'accordéon enfant change, l'accordéon parent ne sait pas qu'il doit restituer et ajuster sa hauteur. L'absence de rendu lorsqu'un accordéon imbriqué est développé ou réduit entraîne l'affichage de la hauteur correcte par l'accordéon parent.

    Solutions possibles

    TL;DR : La solution consiste à informer le parent des changements de hauteur de l'accordéon de l'enfant afin qu'il puisse ajuster sa propre hauteur en conséquence.

    React : Une des techniques mentionnées dans le re-rendu des composants Force | 4 méthodes simples » par Josip Miskovic)

    Vos Accordion 组件可以受益于回调函数 prop,该函数在其高度发生变化时被调用,例如 onHeightChange。然后,在 Portfolio 组件中,您可以通过将新的回调函数传递给 Accordion 组件来将此高度更改向上传播到 Homepage 组件,利用 onHeightChange propriétés.

    手风琴.tsx

    export interface AccordionProps extends Component {
      title: string
      children: ReactNode
      isActive?: boolean
      onClick?: () => void
      onHeightChange?: () => void
      headingSize?: "h1" | "h2"
    }
    
    export function Accordion({
      className = "",
      title,
      children,
      isActive,
      onClick,
      onHeightChange,
      headingSize = "h1",
      testId = "Accordion",
    }: AccordionProps) {
      // ...
    
      useEffect(() => {
        if (isActive === undefined) return
        isActive ? setHeight(`${contentHeight.current?.scrollHeight}px`) : setHeight("0px")
        if(onHeightChange) onHeightChange() // Call the onHeightChange callback
      }, [isActive])
    
      // ...
    }
    

    Modifiez ensuite votre composant Portfolio pour propager l'événement de changement de hauteur :

    export function Portfolio({ className = "", testId = "portfolio", onHeightChange }: PortfolioProps & {onHeightChange?: () => void}) {
      return (
        <AccordionGroup className={`overflow-hidden ${className}`} testId={testId}>
          {projects.map((project, index) => (
            <Accordion 
              title={project.title} 
              key={`${index}-${project}`} 
              headingSize="h2"
              onHeightChange={onHeightChange} // Propagate the height change event
            >
              <ProjectCard project={project} />
            </Accordion>
          ))}
        </AccordionGroup>
      )
    }
    

    Enfin, vous pouvez ajouter une clé à l'accordéon Portfolio sur la page d'accueil qui changera lorsque l'événement de changement de hauteur sera déclenché. Cela entraînera un nouveau rendu de l'accordéon :

    export function Homepage({ className = "", testId = "homepage" }: HomepageProps) {
      const [key, setKey] = useState(Math.random());
      //...
    
      return (
        //...
        <Accordion className="lg:hidden" title="Portfolio" key={key}>
          <Portfolio onHeightChange={() => setKey(Math.random())} />
        </Accordion>
        //...
      )
    }
    

    De cette façon, vous forcerez le composant Accordéon parent à être restitué chaque fois que la hauteur de l'Accordéon enfant change.

    répondre
    0
  • P粉649990273

    P粉6499902732024-02-22 10:08:40

    Vous savez, l'implémentation ici est un peu difficile car lorsque vous souhaitez mettre à jour la hauteur de l'accordéon d'un grand-parent à partir de son accordéon enfant, vous ne pouvez pas vraiment savoir à partir de là quel accordéon de grand-parent correspondant vous souhaitez mettre à jour à moins de transmettre les accessoires à l'accordéon grand-parent, et transmettre les accessoires à un composant intermédiaire (par exemple, Portfolio, le parent de l'accordéon enfant) afin qu'il puisse les propager à ses accordéons enfants.
    En faisant cela, nous pouvons permettre aux grands-parents et aux enfants de communiquer en accordéon d'une manière ou d'une autre.
    Ce n'est peut-être pas la meilleure solution que vous puissiez trouver, mais malheureusement, je ne trouve pas de meilleure solution.


    Donc pour récapituler : l'idée est de créer un état au niveau supérieur pour contenir une référence à la hauteur de chaque accordéon parent, c'est donc un tableau dont la longueur est définie "manuellement", ce qui le rend un peu moche, mais si vous Si vous devez utiliser un tableau de données pour afficher votre composant de manière dynamique, ce n'est pas un problème, comme nous le découvrirons plus tard, nous verrons également les limites de la solution de contournement.


    Solution :

    Nous allons maintenant opter pour la solution la plus simple et la plus directe qui fonctionne pour ce qui est inclus dans la question.

    Comme mentionné ci-dessus, nous créons d'abord l'état dans le composant HomePage :

    const [heights, setHeights] = useState(Array(7).fill("0px")); // you have 7 parent Accordions
    

    Après avoir créé l'état du tableau au niveau supérieur, nous transmettons maintenant la fonction de réglage de l'état à chaque composant Accordéon setHeights、索引 indexx 以及相应的height heightParent s'il s'agit d'un Accordéon parent

    <AccordionGroup>
      <Accordion title="Puyan Wei" heightParent={heights[0]} setHeights={setHeights} indexx="0">
          <PuyanWei setHeights={setHeights} indexx="0" />
      </Accordion>
      <Accordion title="Portfolio" heightParent={heights[1]} setHeights={setHeights} indexx="1">
          <Portfolio setHeights={setHeights} indexx="1" />
      </Accordion>
      //...
      <Accordion title="Socials" heightParent={heights[6]} setHeights={setHeights} indexx="6">
          <Socials setHeights={setHeights} indexx="6" />
      </Accordion> 
    </AccordionGroup>
    

    REMARQUE : Les indexx 属性和传递给中间组件(Portfolio)的 indexx propriétés transmises au parent doivent avoir la même valeur indiquant l'index correspondant, qui est en fait la clé de la solution.
    Nommez-le "indexx" avec deux "x" pour éviter les conflits plus tard.

    Ensuite, transmettez ces accessoires reçus du composant central à l'accordéon enfant :

    export function Portfolio({
      className = "",
      testId = "portfolio",
      indexx,
      setHeight,
    }: PortfolioProps) {
      // update props interface
      return (
        <AccordionGroup className={`overflow-hidden ${className}`} testId={testId}>
          {projects.map((project, index) => (
            <Accordion
              title={project.title}
              key={`${index}-${project}`}
              headingSize="h2"
              indexx={indexx}
              setHeight={setHeight}
            >
              <ProjectCard project={project} />
            </Accordion>
          ))}
        </AccordionGroup>
      );
    }
    

    Maintenant, à partir de votre composant Accordéon enfant, vous pouvez utiliser la propriété indexx transmise pour mettre à jour la hauteur du parent Accordéon correspondant dans l'état de la page d'accueil. Ainsi, lorsque nous mettons à jour la taille de l'enfant, nous mettons également à jour la hauteur du parent

    function handleToggle() {
      if (!contentHeight?.current) return;
      setIsOpen((prevState) => !prevState);
      let newHeight = isOpen ? "0px" : `${contentHeight.current.scrollHeight}px`;
      if (!heightParent) { // there is no need to update the state when it is a parent Accordion
        setHeight(newHeight);
      }
      setHeights((prev) =>
        prev.map((h, index) => (index == indexx ? newHeight : h))
      );
    }
    

    Enfin, lorsque vous spécifiez la hauteur d'un accordéon, vous pouvez vérifier s'il reçoit la heightParent 作为 props,以便我们知道它是父级,这样,我们让 Accordion 组件使用 heightParent 作为 maxHeight 而不是它自己的状态 height(如果它是父状态),这就是忽略更新 height 状态的原因当它是打开的父 Accordion 时,因此,我们必须更改 maxHeight façon dont la propriété est définie, maintenant cela devrait dépendre de la nature de l'accordéon :

    style={{ maxHeight: `${heightParent? heightParent : height }` }}
    

    Si vous voulez que l'accordéon parent utilise simplement son état height 作为 maxHeight et garde le même code, cela a plus de sens

    style={{ maxHeight: height }}
    

    Vous pouvez toujours l'exécuter en ajoutant useEffect 并确保其仅在更新并定义接收到的 heightParent 属性时运行来执行此操作,我们这样做确保代码仅在父手风琴应更新其 height le statut dans le composant Accordéon :

    useEffect(()=>{
     if(heightParent){
       setHeight(heightParent)
     }
    },[heightParent])
    

    Comme mentionné ci-dessus, celui-ci a plus de sens et est aussi le plus joli, mais je préfère quand même le premier car il est plus simple et économise également un rendu supplémentaire.


    Traitement dynamique des données :

    Si nous stockons des données dans un tableau et souhaitons afficher notre composant sur cette base, nous pouvons faire ceci :

    const data = [...]
    const [heights, setHeights] = useState(data.map(_ => "0px")); 
    //...
    <section className="col-span-10 col-start-2">
     <AccordionGroup>
      {data.map(element, index => {
         <Accordion key={index} title={element.title} heightParent={heights[index]} setHeights={setHeights} indexx={index} >
           {element.for === "portfolio" ? <Portfolio setHeights={setHeights} indexx={index} /> : <PuyanWei setHeights={setHeights} indexx={index} /> }  // just an example
         </Accordion>
      })
      }
     </AccordionGroup>
    </section>
    

    Vous pouvez remarquer que nous devons spécifier une propriété key,以便我们可以使用它而不是indexx,但您知道key > dans l'accordéon parent et nous ne voulons pas nous en mêler de toute façon, j'espère que vous comprenez


    Limites :

    Évidemment, cette solution ne fonctionne que pour un seul niveau, donc si le sous-accordéon lui-même devient un sous-accordéon, vous devez l'enrouler à nouveau, mais si je comprends ce que vous faites, vous n'y serez probablement pas confronté, car avec votre implémentation l'accordéon enfant devrait montrer les éléments, mais qui sait, peut-être qu'un jour vous devrez lui faire rendre un autre accordéon enfant, c'est pourquoi je pense que ma suggestion est une solution de contournement et non la meilleure solution.


    Comme je l'ai dit, ce n'est peut-être pas la meilleure solution, mais pour être honnête, surtout pour cette implémentation, Je ne pense pas qu'une telle solution de travail à plusieurs niveaux existe, s'il vous plaît, prouvez-moi le contraire, je suis le post .

    répondre
    0
  • Annulerrépondre