UNPKG

@wix/design-system

Version:

@wix/design-system

167 lines 6.9 kB
import React from 'react'; import PropTypes from 'prop-types'; import debounce from 'lodash/debounce'; import shallowEqual from 'shallowequal'; import { st, classes } from './Ellipsis.st.css.js'; import Tooltip from '../../Tooltip'; import { ZIndex } from '../ZIndex'; import { TooltipCommonProps } from '../PropTypes/TooltipCommon'; import TextComponent from './components/TextComponent'; class Ellipsis extends React.PureComponent { constructor(props) { super(props); /** * Once text component has rendered, * Update text content and tooltip active state * @private */ this._onTextRendered = () => { const { isActive, textContent } = this.state; const newTextContent = this._getTextContent(); const shouldBeActive = this._checkShouldBeActive(); const newState = { textRendered: true, ...(newTextContent !== textContent ? { textContent: newTextContent } : {}), ...(shouldBeActive !== isActive ? { isActive: shouldBeActive } : {}), }; this.setState(oldState => ({ ...oldState, ...newState })); }; /** * An ellipsis is considered active when either the text's scroll width/height is wider than it's container or itself. * @private */ this._updateIsActive = () => { const { isActive } = this.state; const shouldBeActive = this._checkShouldBeActive(); if (shouldBeActive !== isActive) { this.setState({ isActive: shouldBeActive }); } }; this._getTextContent = () => { const { current: textElement } = this.ref; return textElement && textElement.textContent; }; this._checkShouldBeActive = () => this._isOverflowingHorizontally() || this._isOverflowingVertically(); this._isOverflowingHorizontally = () => { const { current: textElement } = this.ref; const { ellipsis } = this.props; const parentWidth = textElement?.parentElement?.offsetWidth; return !!(ellipsis && textElement && ((parentWidth && textElement.scrollWidth - parentWidth > 1) || textElement.offsetWidth < textElement.scrollWidth)); }; this._isOverflowingVertically = () => { const { current: textElement } = this.ref; const { ellipsis, maxLines } = this.props; const parentHeight = textElement?.parentElement?.offsetHeight; return !!(maxLines && maxLines > 1 && ellipsis && textElement && ((parentHeight && textElement.scrollHeight - parentHeight > 1) || textElement.offsetHeight < textElement.scrollHeight)); }; /** * A callback for resizing the window, must be debounced in order to improve performance. * @private */ this._debouncedUpdate = debounce(this._updateIsActive, 100); this.state = { isActive: false, textContent: null, textRendered: false, }; this.ref = React.createRef(); this.tooltipRef = React.createRef(); } componentDidMount() { window.addEventListener('resize', this._debouncedUpdate); this.props.onEllipsisStateChange?.(this.state.isActive); } _renderText() { const { render, ellipsis, maxLines } = this.props; const { textRendered, isActive } = this.state; return (React.createElement(TextComponent, { render, ellipsis, maxLines, textRendered, onTextRendered: this._onTextRendered, textElementRef: this.ref, isEllipsisActive: isActive, tooltipRef: this.tooltipRef })); } render() { const { appendTo, wrapperClassName, disabled, enterDelay, exitDelay, fixed, flip, maxWidth, moveArrowTo, moveBy, onHide, onShow, placement, showTooltip, textAlign, zIndex, size, interactive, } = this.props; const { isActive, textContent } = this.state; return showTooltip && isActive ? (React.createElement(Tooltip, { ref: this.tooltipRef, className: st(classes.tooltip, wrapperClassName), content: textContent, appendTo, disabled, enterDelay, exitDelay, fixed, flip, maxWidth, moveArrowTo, moveBy, onHide, onShow, placement, textAlign, zIndex, size, interactive }, this._renderText())) : (this._renderText()); } static getDerivedStateFromProps(props, state) { const { render, ellipsis, maxLines } = props; const textPropsChanged = state.prevRender !== render || state.prevEllipsis !== ellipsis || state.prevMaxLines !== maxLines; if (!textPropsChanged) { return null; } // Text changed, initialize textRendered state return { textRendered: false, prevRender: render, prevEllipsis: ellipsis, prevMaxLines: maxLines, }; } componentDidUpdate(prevProps, prevState) { const { textRendered, isActive } = this.state; if (textRendered && !shallowEqual(prevProps, this.props)) { this._updateIsActive(); } if (prevState.isActive !== isActive && this.props.onEllipsisStateChange) { this.props.onEllipsisStateChange(this.state.isActive); } } componentWillUnmount() { this._debouncedUpdate.cancel(); window.removeEventListener('resize', this._debouncedUpdate); } } Ellipsis.propTypes = { /** When true, text that is longer than it's container will be truncated to a single line followed by ellipsis. Otherwise the text will break into several lines. */ ellipsis: PropTypes.bool, /** True by default, set it to false in order to show ellipsis without a tooltip. */ showTooltip: PropTypes.bool, /** A className to be applied to the Ellipsis wrapper. */ wrapperClassName: PropTypes.string, /** The render function, use it to render a text you want to truncate with ellipsis. */ render: PropTypes.func, /** maxLines truncates text at a specific number of lines. */ maxLines: PropTypes.number, /** A callback fired when ellipsis state changes. */ onEllipsisStateChange: PropTypes.func, // Tooltip props ...TooltipCommonProps, }; Ellipsis.defaultProps = { ellipsis: false, appendTo: 'window', flip: false, fixed: false, placement: 'top', zIndex: ZIndex.tooltip, enterDelay: 0, exitDelay: 0, showTooltip: true, }; export default Ellipsis; //# sourceMappingURL=Ellipsis.js.map