UNPKG

wix-style-react

Version:
213 lines • 8.53 kB
import React from 'react'; import PropTypes from 'prop-types'; import debounce from 'lodash/debounce'; import shallowEqual from 'shallowequal'; import { st, classes, vars } from './Ellipsis.st.css'; import Tooltip from '../../Tooltip'; import { ZIndex } from '../../ZIndex'; import { TooltipCommonProps } from '../PropTypes/TooltipCommon'; const isTestEnv = process.env.NODE_ENV === 'test'; class TextComponent extends React.PureComponent { constructor() { super(...arguments); this.requestedRecalculationMount = null; this.requestedRecalculationUpdate = []; this._getEllipsisClasses = () => { const { ellipsis, maxLines } = this.props; const ellipsisLines = maxLines > 1 ? 'multiline' : 'singleLine'; return className => ellipsis ? st(classes.text, { ellipsisLines }, className) : className; }; } componentDidMount() { if (!this.props.textRendered) { /** * The requestAnimationFrame implementation is meant for browser only race condition bug fix. * It does not get reproduced in test environment due JSDOM being not a browser. * So we skip the requestAnimationFrame part in order not to have delays in calculations. */ if (isTestEnv) { return this.props.onTextRendered(); } this.requestedRecalculationMount = requestAnimationFrame(() => { this.props.onTextRendered(); }); } } componentDidUpdate() { if (!this.props.textRendered) { /** * The requestAnimationFrame implementation is meant for browser only race condition bug fix. * It does not get reproduced in test environment due JSDOM being not a browser. * So we skip the requestAnimationFrame part in order not to have delays in calculations. */ if (isTestEnv) { return this.props.onTextRendered(); } this.requestedRecalculationUpdate.push(requestAnimationFrame(() => { this.props.onTextRendered(); })); } } componentWillUnmount() { if (this.requestedRecalculationMount) { cancelAnimationFrame(this.requestedRecalculationMount); } if (this.requestedRecalculationUpdate.length !== 0) { this.requestedRecalculationUpdate.forEach(id => cancelAnimationFrame(id)); } } render() { const { render, maxLines, textElementRef } = this.props; return render({ ref: textElementRef, ellipsisClasses: this._getEllipsisClasses(), ellipsisInlineStyle: { [vars.maxLines]: maxLines }, }); } } 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 newState = { textRendered: true }; const newTextContent = this._getTextContent(); if (newTextContent !== textContent) { newState.textContent = newTextContent; } const shouldBeActive = this._checkShouldBeActive(); if (shouldBeActive !== isActive) { newState.isActive = shouldBeActive; } this.setState(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; return (ellipsis && textElement && (textElement.scrollWidth - textElement.parentNode.offsetWidth > 1 || textElement.offsetWidth < textElement.scrollWidth)); }; this._isOverflowingVertically = () => { const { current: textElement } = this.ref; const { ellipsis, maxLines } = this.props; return (maxLines > 1 && ellipsis && textElement && (textElement.scrollHeight - textElement.parentNode.offsetHeight > 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(); } componentDidMount() { window.addEventListener('resize', this._debouncedUpdate); } _renderText() { const { render, ellipsis, maxLines } = this.props; const { textRendered } = this.state; return (React.createElement(TextComponent, { render, ellipsis, maxLines, textRendered, onTextRendered: this._onTextRendered, textElementRef: this.ref })); } render() { const { appendTo, wrapperClassName, disabled, enterDelay, exitDelay, fixed, flip, maxWidth, moveArrowTo, onHide, onShow, placement, showTooltip, textAlign, zIndex, size, } = this.props; const { isActive, textContent } = this.state; return showTooltip && isActive ? (React.createElement(Tooltip, { className: st(classes.tooltip, wrapperClassName), content: textContent, appendTo, disabled, enterDelay, exitDelay, fixed, flip, maxWidth, moveArrowTo, onHide, onShow, placement, textAlign, zIndex, size }, 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) { const { textRendered } = this.state; if (textRendered && !shallowEqual(prevProps, this.props)) { this._updateIsActive(); } } 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, // 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