const TRUNCATE_WORDS_CUTOFF = 12;
export const truncateName = (name: string, truncateCutoff: number = TRUNCATE_WORDS_CUTOFF) =>
name.length > truncateCutoff
? `${name.substring(0, truncateCutoff)}...`
: name;
But depending on the font, characters in the string will have different widths. We don't necessarily want to hardcode the cutoff.
To solve this problem, there's a CSS approach and a hash table approach where we map each characters to a weight representing its width (highly dependent on the font). The hash table approach is overkill. The CSS approach is simpler. Simply add the following attribute:
.truncatedName {
width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
render() {
const { name } = this.props;
return <div className={styles.truncatedName}>{name}</div>
}
For more on CSS truncation of text, see this
If we also want to detect when the string truncation has occurred, we can use ref callback
const MAX_DISPLAY_NAME_WIDTH = 100; // number based on the CSS rule for truncating
constructor(props: {name: string}) {
super(props);
this.state = {
displayTooltip: false
};
}
getDisplayNameWidth = (element: HTMLInputElement) => {
if (element) {
const { width } = element.getBoundingClientRect();
if (width >= MAX_DISPLAY_NAME_WIDTH) {
this.setState({ displayTooltip: true });
}
}
};
renderDisplayName(name: string) {
return <div className={styles.truncatedName} ref={this.getDisplayNameWidth}>{name}</div>
}
render() {
const { name } = this.props;
const { displayTooltip } = this.state;
if (displayTooltip) {
return (
<Tooltip title={name}>
{this.renderDisplayName(name)}
</Tooltip>
);
}
return this.renderDisplayName(name);
}
The better solution outlined above provides a way to detect if the CSS truncation occurred during initial mount, but it doesn't account for changes to the name length after component's initial mount. An improvement upon the previous solution is as follows:
const MAX_DISPLAY_NAME_WIDTH = 100; // number based on the CSS rule for truncating
private displayNameRef: React.RefObject<HTMLInputElement> = React.createRef();
constructor(props: {name: string}) {
super(props);
this.state = {
displayTooltip: false
};
}
componentDidMount() {
this.updateDisplayTooltip();
}
componentDidUpdate() {
this.updateDisplayTooltip();
}
updateDisplayTooltip() {
if (!this.displayNameRef.current) return;
const { width } = this.displayNameRef.current.getBoundingClientRect();
this.setState({ displayTooltip: width >= MAX_DISPLAY_NAME_WIDTH });
}
renderDisplayName(name: string) {
return <div className={styles.truncatedName} ref={this.displayNameRef}>{name}</div>
}
render() {
const { name } = this.props;
const { displayTooltip } = this.state;
return displayTooltip ?
(
<Tooltip title={name}>
{this.renderDisplayName(name)}
</Tooltip>
) : this.renderDisplayName(name);
}
According to the official documentation for React Refs and DOM,
React will assign the current property with the DOM element when the component mounts, and assign it back to null when it unmounts. ref updates happen before componentDidMount or componentDidUpdate lifecycle methods.
This is why we need to update displayTooltip
in both componentDidMount
and componentDidUpdate
methods.