我正在嘗試建立我的主頁的行動版本,我的嵌套手風琴「專案」似乎有一個錯誤,它在第一次打開時無法顯示底部專案部分的正確高度。
要打開它,您首先單擊項目文本,然後它列出項目,然後單擊項目切換項目卡。
(已更新)我相信發生這種情況是因為當子手風琴打開時,我的父手風琴沒有重新更新其高度。
你知道這樣做的好方法嗎?或者如果需要的話,我應該以一種使這成為可能的方式重組我的組件嗎?困難在於 Accordion 接受兒童,並且我在其中重複使用 Accordion,所以它相當混亂。我知道我可以使用回調函數來觸發父級,但不太確定如何解決這個問題。
首頁.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> ) }
投資組合.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 - AccordionGroup 的目的是只允許一次開啟一個子 Accordion。如果 Accordion 不在 AccordionGroup 中,它可以獨立開啟和關閉。
"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> ) }
手風琴.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> }
如有任何幫助,我們將不勝感激,謝謝!
P粉9981006482024-02-22 10:41:59
TL;DR:父手風琴需要了解這些變化,以便它可以相應地調整自己的高度。
我想您可能正在使用amiut/accordionify
,如圖所示透過“創建輕量級React Accordions”來自Amin A. Rezapour。
這是我發現的唯一一個使用 AccordionGroup
的專案。
應用中的嵌套手風琴結構涉及父子關係,其中子手風琴的高度會根據展開還是折疊而動態變化。
這可以透過您的Portfolio.tsx
來說明,其中AccordionGroup
元件包含多個基於 建立的
數組。這些 Accordion
元件項目Accordion
組件是提到的「子」手風琴:
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> ) }
每個子Accordion
都包含一個顯示項目詳細資訊的ProjectCard
。當使用者點擊Accordion
(或「專案」)時,它會展開以顯示ProjectCard
。
這就是高度變化發揮作用的地方;手風琴將根據用戶互動展開或折疊,動態改變其高度。
動態高度在 Accordion.tsx
中管理:
function handleToggle() { if (!contentHeight?.current) return setIsOpen((prevState) => !prevState) setHeight(isOpen ? "0px" : `${contentHeight.current.scrollHeight}px`) if (onClick) onClick() }
當呼叫handleToggle
函數時,它會檢查手風琴目前是否開啟(isOpen
)。如果是,則高度設定為「0px」(即,手風琴折疊)。如果未開啟,則高度設定為內容的滾動高度(即展開手風琴)。
這些兒童手風琴的動態高度變化是您遇到的問題的關鍵部分。父手風琴需要了解這些變化,以便它可以相應地調整自己的高度。
我在同一個Accordion.tsx
中看到:
useEffect(() => { if (isActive === undefined) return isActive ? setHeight(`${contentHeight.current?.scrollHeight}px`) : setHeight("0px") }, [isActive])
手風琴的高度是根據 isActive
屬性設定的,它表示手風琴目前是否打開。如果打開,則高度設定為手風琴內容的滾動高度(有效展開手風琴),如果未激活,則高度設置為 0px
(折疊手風琴)。
但是,雖然此效果根據每個手風琴自身的 isActive
狀態正確調整其高度,但它並未考慮子手風琴高度的變化。
當嵌套(子)手風琴改變其高度(由於展開或折疊)時,父手風琴的高度不會重新計算,因此不會調整以適應父手風琴的新高度孩子。
換句話說,當子手風琴的高度改變時,父手風琴並不知道它需要重新渲染並調整其高度。當嵌套的手風琴展開或折疊時缺乏重新渲染會導致父手風琴無法顯示正確的高度。
TL;DR:解決方案包括讓父級意識到子手風琴的高度變化,以便它可以相應地調整自己的高度。
(“React:強制元件重新渲染中提到的技術之一| 4 個簡單方法”,來自 Josip Miskovic)
您的 Accordion
元件可以受益於回調函數 prop,該函數在其高度變化時被調用,例如 onHeightChange
。然後,在Portfolio
元件中,您可以透過將新的回呼函數傳遞給Accordion
元件來將此高度變更向上傳播到Homepage
元件,利用onHeightChange
屬性。
手風琴.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]) // ... }
然後修改您的 Portfolio 元件以傳播高度更改事件:
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> ) }
最後,您可以在主頁上的 Portfolio 手風琴添加一個鍵,該鍵將在觸發高度更改事件時發生變化。這將導致手風琴重新渲染:
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> //... ) }
這樣,只要子 Accordion 的高度發生變化,您就會強制重新渲染父 Accordion 元件。
P粉6499902732024-02-22 10:08:40
您知道,這裡的實現有點具有挑戰性,因為當您想要從其子手風琴更新祖父母手風琴的高度時,您無法真正從那裡知道您想要更新哪個相應的祖父母手風琴除非您將道具傳遞給祖父母手風琴,並將道具傳遞給中間組件(例如,Portfolio
,即子手風琴的父級),以便它可以將它們傳播到其子手風琴。
透過這樣做我們可以讓祖父母和孩子手風琴以某種方式進行交流。
也許這不是您能找到的最佳解決方案,但遺憾的是,我想不出更好的解決方案。
所以回顧一下:這個想法是在頂層創建一個狀態來保存引用每個父手風琴的高度,所以它是一個數組,其中長度是「手動」設置的,這使得它在某種程度上很難看,但是如果你必須使用資料數組來動態顯示您的元件,那麼這不是問題,我們稍後會發現,我們也會看到解決方法的限制。
現在我們將採用最簡單、最直接的修復方法,適用於問題中包含的內容。
如上所述,首先我們在 HomePage 元件中建立狀態:
const [heights, setHeights] = useState(Array(7).fill("0px")); // you have 7 parent Accordions
在頂層建立陣列狀態後,現在,我們向每個Accordion 元件傳遞狀態設定函數setHeights
、索引indexx
以及對應的height heightParent
如果它是父Accordion
<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>
注意: 傳遞給父級的indexx
屬性和傳遞給中間元件(Portfolio)的indexx
屬性它們應該具有相同的值表示對應的索引,這實際上是解決方案的關鍵。
命名為“indexx”,其中帶有兩個“x”,以避免以後發生衝突。
然後,從中間組件將這些收到的道具傳遞給子手風琴:
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> ); }
現在,從您的子Accordion 元件中,您可以利用傳遞的indexx
屬性來更新HomePage 狀態下對應Accordion 父級的高度,因此當我們更新子高度時,我們也會更新父高度
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)) ); }
最後,當您指定一個Accordion 的高度時,您可以檢查它是否接收heightParent
作為props,以便我們知道它是父級,這樣,我們讓Accordion 元件使用 heightParent
作為maxHeight
而不是它自己的狀態height
(如果它是父狀態),這就是忽略更新height
狀態的原因當它是打開的當父Accordion 時,因此,我們必須更改maxHeight
屬性的設定方式,現在它應該取決於Accordion 的性質:
style={{ maxHeight: `${heightParent? heightParent : height }` }}
如果您想讓父 Accordion 只需使用其狀態 height
作為 maxHeight
# 並保持程式碼不變,這樣更有意義
style={{ maxHeight: height }}
您仍然可以透過在Accordion 元件中新增useEffect
並確保其僅在更新並定義接收到的heightParent
屬性時運行來執行此操作,我們請確保程式碼僅在父手風琴應更新其height
狀態時運行:
useEffect(()=>{ if(heightParent){ setHeight(heightParent) } },[heightParent])
如上所述,這更有意義,而且也最漂亮,但我仍然更喜歡第一個,因為它更簡單,並且還節省了一個額外的渲染。
如果我們將資料儲存在數組中,並且希望基於此顯示我們的元件,則可以這樣做:
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>
您可以注意到,我們必須在父手風琴中指定一個key
#,以便我們可以使用它而不是indexx
,但您知道key
>財產很特殊,無論如何我們都不想弄亂它,希望您明白
很明顯,這個解決方案僅適用於一個級別,因此,如果子手風琴本身成為子手風琴,那麼您必須再次圍繞它,但如果我理解您在做什麼,您可能就不會面對這種情況,因為透過您的實現,子Accordion 應該顯示項目,但誰知道也許有一天您需要讓它返回另一個子Accordion,這就是為什麼我認為我的建議是一種解決方法而不是最佳解決方案。
就像我說的,這可能不是最好的解決方案,但說實話,尤其是對於這個實現,我認為不存在這樣的多層工作解決方案,請證明我錯了,我我正在關注該帖子。