Matt
Matt

Reputation: 5428

How can I conditionally show title tooltip if CSS ellipsis is active within a React component

Problem:

I'm looking for a clean way to show a title tooltip on items that have a CSS ellipsis applied. (Within a React component)

What I've tried:

I setup a ref, but it doesn't exist until componentDidUpdate, so within componentDidUpdate I forceUpdate. (This needs some more rework to handle prop changes and such and I would probably use setState instead.) This kind of works but there are a lot of caveats that I feel are unacceptable.

  1. setState/forceUpdate - Maybe this is a necessary evil
  2. What if the browser size changes? Do I need to re-render with every resize? I suppose I'd need a debounce on that as well. Yuck.

Question:

Is there a more graceful way to accomplish this goal?

Semi-functional MCVE:

https://codepen.io/anon/pen/mjYzMM

class App extends React.Component {
  render() {
    return (
      <div>
        <Test message="Overflow Ellipsis" />
        <Test message="Fits" />
      </div>
    );
  }
}

class Test extends React.Component {
  constructor(props) {
    super(props);
    this.element = React.createRef();
  }
  componentDidMount() {
    this.forceUpdate();
  }

  doesTextFit = () => {
    if (!this.element) return false;
    if (!this.element.current) return false;
    console.log(
      "***",
      "offsetWidth: ",
      this.element.current.offsetWidth,
      "scrollWidth:",
      this.element.current.scrollWidth,
      "doesTextFit?",
      this.element.current.scrollWidth <= this.element.current.offsetWidth
    );

    return this.element.current.scrollWidth <= this.element.current.offsetWidth;
  };
  render() {
    return (
      <p
        className="collapse"
        ref={this.element}
        title={this.doesTextFit() ? "it fits!" : "overflow"}
      >
        {this.props.message}
      </p>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("container"));
.collapse {
    width:60px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="container"></div>

Upvotes: 7

Views: 6675

Answers (3)

Nikita Rybak
Nikita Rybak

Reputation: 68006

Using ref callbacks works quite well and does not add a lot of overhead.

const Component = (props) => {
  const onRef = (node) => {
    if (!node) {
      return;
    }
    const isOverflowing = node.clientWidth !== node.scrollWidth;
    if (isOverflowing && selectedStatusName) {
      node.setAttribute('title', selectedStatusName);
    } else {
      node.removeAttribute('title');
    }
  }

  return (<div ref={onRef}>...</div>)
}

Upvotes: 1

Joackim Pennerup
Joackim Pennerup

Reputation: 81

I use this framework agnostic snippet to this. Just include it on your page and see the magic happen ;)

(function() {
  let lastMouseOverElement = null;
  document.addEventListener("mouseover", function(event) {
    let element = event.target;
    if (element instanceof Element && element != lastMouseOverElement) {
        lastMouseOverElement = element;
        const style = window.getComputedStyle(element);
        const whiteSpace = style.getPropertyValue("white-space");
        const textOverflow = style.getPropertyValue("text-overflow");
        if (whiteSpace == "nowrap" && textOverflow == "ellipsis" && element.offsetWidth < element.scrollWidth) {
            element.setAttribute("title", element.textContent);
        } else {
            element.removeAttribute("title");
        }
    }
  });
})();

From: https://gist.github.com/JoackimPennerup/06592b655402d1d6181af32def40189d

Upvotes: 2

Matt
Matt

Reputation: 5428

Since a lot of people are still viewing this question. I did finally figure out how to do it. I'll try to rewrite this into a working example at some point but here's the gist.

// Setup a ref
const labelRef = useRef(null);

// State for tracking if ellipsis is active
const [isEllipsisActive, setIsEllipsisActive] = useState(false);

// Setup a use effect
useEffect(() => {
    if(labelRef?.current?.offsetWidth < labelRef?.current?.scrollWidth) {
        setIsEllipsisActive(true);
    }
}, [labelRef?.current, value, isLoading]); // I was also tracking if the data was loading

// Div you want to check if ellipsis is active
<div ref={labelRef}>{value}</div>

Upvotes: 6

Related Questions