Adding ellipsis and tooltip after two lines text - React

Anand Dev Singh picture Anand Dev Singh · Jan 1, 2019 · Viewed 12k times · Source

Is it possible to create a React component that can add the ellipsis after two lines and show the tooltip only if the text is wrapped?

I've tried customizing the Material UI's Typography component with "noWrap" property and additional CSS to it, but failed.

Please assist, I want to achieve something like this screen: enter image description here

Answer

Ryan Cogswell picture Ryan Cogswell · Jan 1, 2019

There are two main aspects to this problem:

  • Show ellipsis for text overflow based on vertical overflow in order to allow more than one line but not unlimited
  • Detect the vertical overflow and include Tooltip functionality in that case

I don't believe FrankerZ's solution will show the ellipsis. Far as I can tell, text-overflow: ellipsis only works for horizontal overflow which requires limiting the text to a single line.

I found a solution for doing ellipsis on vertical overflow here, but it may require significant tweaking once you start wrapping this in Material UI components (e.g. ListItem) that bring additional CSS into the mix, and it may get complicated enough to not be worth it. This solution has the effect of reserving space for the ellipsis at the end of each line of text which doesn't seem ideal. There seem to be some other solutions to this problem out there, but I have not used any of them (including this one other than today) myself.

The second part (detecting the overflow) seems to be more straightforward and is handled by divRef.current.scrollHeight > divRef.current.offsetHeight. I came to this solution by finding many references to doing a similar condition based on width. I have not personally used this technique outside of today when working on this answer, so someone with deeper CSS expertise might chime in with "you should never do that because of ...", but it seems to work (I haven't done any significant browser testing -- only tried it on Chrome).

I'm using hooks in my example for the syntax convenience, so if you aren't using the alpha you'll need to translate the state, ref, and effect work into the corresponding class counterparts. This also isn't currently dealing with resizing the window which would require re-evaluating whether the tooltip should be in effect. With those caveats, hopefully this will give you a few steps toward a workable solution.

Here's the code:

import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import Tooltip from "@material-ui/core/Tooltip";
import { withStyles } from "@material-ui/core/styles";
/* 
CSS from http://hackingui.com/front-end/a-pure-css-solution-for-multiline-text-truncation/ 
Additional syntax help from https://stackoverflow.com/questions/40965977/cant-target-before-pseudo-selector-in-jss
*/
const styles = theme => ({
  listItem: {
    maxWidth: "20rem",
    overflow: "hidden",
    position: "relative",
    lineHeight: "1.2em",
    maxHeight: "2.4em",
    textAlign: "justify",
    marginRight: "-1em",
    paddingRight: "1em",
    borderBottom: "1px solid",
    marginBottom: "0.5em",
    "&&:before": {
      content: '"..."',
      position: "absolute",
      right: 0,
      bottom: 0
    },
    "&&:after": {
      content: '""',
      position: "absolute",
      right: 0,
      width: "1em",
      height: "1em",
      marginTop: "0.2em",
      background: "white"
    }
  }
});
const data = [
  "Some short text",
  "Some text that is a little bit longer",
  "Some text that will need to wrap but still fits on two lines",
  "Some text that will overflow because it is more than just two lines worth when the maxWidth is set at 20rem.",
  "A massive range of hammer drill machines and rotary hammers for SDS-plus accessory tools, designed for higher performance drilling and longer life - for easy drilling in concrete and other materials."
];
const TooltipDiv = props => {
  const divRef = useRef(null);
  const [allowTooltip, setAllowTooltip] = useState(false);
  useEffect(() => {
    if (
      !allowTooltip &&
      divRef.current.scrollHeight > divRef.current.offsetHeight
    ) {
      setAllowTooltip(true);
    }
  }, []);
  if (allowTooltip) {
    return (
      <Tooltip title={props.text}>
        <div ref={divRef} className={props.className}>
          {props.text}
        </div>
      </Tooltip>
    );
  }
  return (
    <div ref={divRef} className={props.className}>
      {props.text}
    </div>
  );
};
function App(props) {
  return (
    <>
      {data.map(text => {
        return (
          <>
            <TooltipDiv text={text} className={props.classes.listItem} />
          </>
        );
      })}
    </>
  );
}
const StyledApp = withStyles(styles)(App);
const rootElement = document.getElementById("root");
ReactDOM.render(<StyledApp />, rootElement);

You can see it in action here:

Edit 4xn3386z6x