首页 > 解决方案 > 使用 React,如何实现具有 css 转换切换(从 fullHeight 到 0px)的基本 Accordion(允许高度保持动态/自动)

问题描述

为了仅使用 HTML 将高度从 auto 转换为 0,CSS 有自己的试验(css 转换不适用于 auto 值)。我使用 Javascript 解决了这个问题,通过...

步骤 1. 为内容高度设置 CSS 转换

步骤 2. 查找内容的实际高度(使用 javascript content.style.offsetHeight

步骤 3. 然后将 设置content.style.height为绝对值(内容的 fullHeight,而不是 auto)。

第 4 步。确保浏览器通过使用强制 DOM 重排content.clientWidth(例如强制页面重排)按页面注册此更改)

第 5 步。 然后立即将高度设置为 0px。content.style.height = 0(这将转换开始转换)

问题

但是,现在我正在尝试在 onlick 处理程序中在 React 中编写相同的序列...

第 1 步(我可以) - 仍然使用在高度上设置的 css 过渡

第 2 步(我可以) - 使用 {refs} 获取content.offsetHeight.

第 3 步(我可以“设置”高度 -它不会同步更新) - 使用 setState 将异步排队要更新/渲染的元素高度,但不会在第 5 步中的关键行之前。

第 4 步 - 在下一个第 5 步之前,元素需要已经将其高度值更改为绝对初始值(在第 3 步中设置)。因此,第 4 步表示在下一步之前将刷新此更改的内容。

第 5 步 - 然后将高度更改为绝对最终值(这将触发转换 - IF 且仅当第 3 步实际生效时)(记住css 转换只会在元素值从绝对值更改为另一个时发生绝对(不适用于自动值)。

我使用 React 编写了类似的代码(见下文),但由于明显的原因它不起作用(如果您了解代码(没有 React)为什么起作用......它需要立即(同步)将高度更新为绝对值(并且还显示属性)在同一事件处理程序中的几个关键时刻,以便转换工作)。

因为改变高度的必要尝试被困在 Reacts异步抽象后面,所以不得不依靠 setState 来做需要在下一行代码之前立即(同步)完成的事情意味着我无法触发转换(因为我的非-React 版本可以)。


工作代码(不使用 React)链接:

JSfiddle https://jsfiddle.net/mattlusty/rfdvbz16/6/

GitHub https://github.com/mattlusty/toggle-with-react.git

document.querySelector("button").addEventListener("click", () => {
  let content = document.querySelector(".content");
  let wasOpen = content.classList.contains("open");
  content.classList.toggle("open");
  // NB: STEP 1 was in the css (transition: height 0.3s)
  // STEP 2.1
  // ensures it is visible
  content.classList.add("block");

  // STEP 2.2
  // now can get the actual full auto height
  let fullHeight = content.offsetHeight;

  if (wasOpen) {
    // STEP 3
    // set initial height explicitly
    content.style.height = fullHeight + "px";
    // STEP 4
    // force reflow
    content.offsetWidth;
    // STEP 5
    // set final height explicitly - which it will transition to
    content.style.height = "0px";
    // STEP 6
    // after transition transitionEnd event handler resets height to auto
    // .. (and removes the forced display:block)
  } else {
    // (Similar steps as other part of IF block) ...
    // set initial height explicitly
    content.style.height = "0px";
    // force reflow
    content.offsetWidth;
    // set final height explicitly - which it will transition to
    content.style.height = fullHeight + "px";
  }
});

document
  .querySelector(".content")
  .addEventListener("transitionend", (event) => {
    event.target.classList.remove("block");
    event.target.style.height = null;
  });

尝试/不工作的代码(与 React 一起使用)链接:

JSFiddle https://jsfiddle.net/mattlusty/L21m0uyg/29

GitHub https://github.com/mattlusty/toggle-with-react.git

import "./app.css";
import React, { Component } from "react";

class App extends Component {
  state = {
    height: null,
    open: true,
    displayBlock: "",
  };

  constructor(props) {
    super(props);
    // to access the elements property offsetHeight in later lifecycle methods
    this.myRef = React.createRef();
  }

  componentDidMount() {
    this.myRef.current.addEventListener("transitionEnd", function () {
      this.setState({ displayBlock: "" });
      this.setState({ height: null });
    });
  }

  toggle = () => {
    let content = this.myRef.current;
    let wasOpen = this.state.open;
    this.setState({ open: !wasOpen });

    // STEP 2.1
    // This *would* set class to ensure display:block is set (to ensure it is visible)
    // ISSUE 1: however in React setState is asynchronous and this it won't be rendered in time to be useful for the next change a couple of lines later!
    this.setState({ displayBlock: true });

    // STEP 2.2
    // *would* now be able to get the actual full natural height
    //(ISSUE 1: in React the above change has not rendered in time for this to get what we need!)
    let fullHeight = content.offsetHeight;

    if (wasOpen) {
      // STEP 3
      // This *would* set initial height to the the required absolute height value (fullHeight)
      // ISSUE 2: Even if we had the fullHeight value from above  - setting it won't ...
      // ... take effect in time for the next steps
      this.setState({ height: fullHeight });

      // STEP 4
      // I *would* have now ensured the elements height updated immedietly, by using following line
      // ISSUE 3.1: but the above setState heights will not have updated the elements yet anyway
      // ... content.offsetWidth;

      // STEP 5
      // This *would* set final height to the the required absolute height value (0px)
      // ... and thus browser *would* detect the change triggering the transition
      // ISSUE 3.2: but the above setState heights will not have updated the elements yet anyway
      // ... thus when React renders the element - browser only sees change from height: auto to finalHeight
      // ... transitions dont work with auto values ... so no transition!
      this.setState({ height: "0px" });

      // STEP 6
      // ISSUE 4
      // If transition happens at this point (which is does not in React - because of the above issues)
      // ... transitionEnd event handler (above) would ...
      // 1) remove any explicit height so it returns to auto
      // ... this.setState({ height: null });
      // 2) remove class "block" which was only for ensuring visibilty whilst height is in transition ...
      // ... this.setState({ displayBlock: null });
    } else {
      // (Similar steps as other part of IF block) ...
      // *would* set initial height to the the required intial absolute height value (0px)
      this.setState({ height: "0px" });

      // the setState above has not changed the elements style yet ...
      // ... so the below wont trigger transition
      this.setState({ height: fullHeight });

      // If transition happens at this point (which is does not in React - because of the above issues)
      // ... transitionEnd event handler (above) would ...
      // 1) remove any explicit height so it returns to auto
      // ... this.setState({ height: null });
      // 2) remove class "block" which was only for ensuring visibilty whilst height is in transition ...
      // ... this.setState({ displayBlock: null });
    }
  };

  render() {
    let innerStyle = { height: this.state.height };
    let classes = `content ${this.state.displayBlock ? "block" : ""} ${
      this.state.open ? "open" : ""
    }`;

    return (
      <React.Fragment>
        <button className="button" onClick={this.toggle}>
          Toggle
        </button>
        <div className={classes} style={innerStyle} ref={this.myRef}>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
          minim veniam, quis nostrud exercitation ullamco laboris nisi ut
          aliquip ex ea commodo consequat. Duis aute irure dolor in
          reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
          culpa qui officia deserunt mollit anim id est laborum.
        </div>
      </React.Fragment>
    );
  }
}

export default App;

标签: javascripthtmlcssreactjs

解决方案


我想你正在寻找ResizeObserver

https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver

 componentDidMount() {
      let div = document.querySelector(id);  // <--- pass element id to observe
            this.resizeObserver.observe(div);
        }
    
  resizeObserver = new ResizeObserver(entries => {
            for (let entry of entries) {
                let width = entry.contentRect.width;
               this.setState({ width: width });
            }
        });

推荐阅读