UNPKG

lucid-ui

Version:

A UI component library from Xandr.

221 lines (219 loc) 8.57 kB
import _, { omit } from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; import ContextMenu from '../ContextMenu/ContextMenu'; import CloseIcon from '../Icon/CloseIcon/CloseIcon'; import * as reducers from './ToolTip.reducers'; import { lucidClassNames } from '../../util/style-helpers'; import { findTypes } from '../../util/component-types'; import { buildModernHybridComponent } from '../../util/state-management'; const cx = lucidClassNames.bind('&-ToolTip'); const flyOutCx = cx.bind('&-FlyOut'); const { bool, func, node, number, object, oneOf, string, oneOfType } = PropTypes; const { Target, FlyOut } = ContextMenu; const ToolTipTarget = (_props) => null; ToolTipTarget.displayName = 'ToolTip.Target'; ToolTipTarget.peek = { description: `The hover target that will trigger the \`ToolTip\` to be displayed.`, }; ToolTipTarget.propName = 'Target'; const ToolTipTitle = (_props) => null; ToolTipTitle.displayName = 'ToolTip.Title'; ToolTipTitle.peek = { description: `A not recommended title, optionally displayed at the top of the \`ToolTip\`.`, }; ToolTipTitle.propName = 'Title'; const ToolTipBody = (_props) => null; ToolTipBody.displayName = 'ToolTip.Body'; ToolTipBody.peek = { description: `The body of the \`ToolTip\`.`, }; ToolTipBody.propName = 'Body'; /** TODO: Remove nonPassThroughs when the component is converted to a functional component */ export const nonPassThroughs = [ 'children', 'className', 'isCloseable', 'isLight', 'onClose', 'style', 'flyOutStyle', 'flyOutMaxWidth', 'direction', 'alignment', 'isExpanded', 'onMouseOver', 'onMouseOut', 'portalId', 'Title', 'Body', 'Target', 'initialState', ]; class ToolTip extends React.Component { constructor(props) { super(props); this.handleMouseOut = (event) => { setTimeout(() => { const { props, state: { isMouseOverFlyout, isMouseOverTarget }, props: { onMouseOut }, } = this; if (!isMouseOverFlyout && !isMouseOverTarget) { onMouseOut && onMouseOut({ props, event }); } }, 100); }; this.handleMouseOverFlyout = () => { this.setState({ isMouseOverFlyout: true }); }; this.handleMouseOutFlyout = (event) => { this.setState({ isMouseOverFlyout: false }); this.handleMouseOut(event); }; this.handleMouseOverTarget = (event) => { this.setState({ isMouseOverTarget: true }); this.props.onMouseOver && this.props.onMouseOver({ props: this.props, event }); }; this.handleMouseOutTarget = (event) => { this.setState({ isMouseOverTarget: false }); this.handleMouseOut(event); }; this.handleClose = ({ event, props, }) => { this.props.onClose && this.props.onClose({ event, props: this.props }); }; this.state = { isMouseOverFlyout: false, isMouseOverTarget: false, isExpanded: false, }; } render() { const { className, alignment, direction, flyOutMaxWidth, flyOutStyle, isCloseable, isExpanded, isLight, portalId, style, ...passThroughs } = this.props; const targetProps = _.first(_.map(findTypes(this.props, ToolTip.Target), 'props')); const title = _.get(_.first(_.map(findTypes(this.props, ToolTip.Title), 'props')), 'children'); const body = _.get(_.first(_.map(findTypes(this.props, ToolTip.Body), 'props')), 'children'); const getAlignmentOffset = (n) => alignment === ContextMenu.CENTER ? 0 : alignment === ContextMenu.START ? n / 2 - 22.5 : -(n / 2 - 22.5); return (React.createElement(ContextMenu, { className: cx('&', className), // WARNING: Alignment is always set to center because the getAlignmentOffset function // handles the alignment instead of delegating to ContextMenu alignment: ContextMenu.CENTER, direction: direction, directonOffset: 15, getAlignmentOffset: getAlignmentOffset, isExpanded: isExpanded, style: style, portalId: portalId, ...omit(passThroughs, nonPassThroughs), onMouseOver: this.handleMouseOverTarget, onMouseOut: this.handleMouseOutTarget }, React.createElement(Target, { ...targetProps, className: cx(_.get(targetProps, 'className'), '&-Target') }, _.get(targetProps, 'children')), React.createElement(FlyOut, { style: { ...flyOutStyle, maxWidth: flyOutMaxWidth || (flyOutStyle && flyOutStyle.maxWidth) || 200, }, className: flyOutCx(className, '&', `&-${direction}`, `&-${alignment}`, isLight ? '&-light' : '&-default'), onMouseOver: this.handleMouseOverFlyout, onMouseOut: this.handleMouseOutFlyout }, isCloseable ? (React.createElement(CloseIcon, { isClickable: true, size: 8, onClick: this.handleClose, className: flyOutCx('&-close') })) : null, !_.isNil(title) ? (React.createElement("h2", { className: flyOutCx('&-Title') }, title)) : null, body))); } } ToolTip.displayName = 'ToolTip'; ToolTip.Title = ToolTipTitle; ToolTip.Target = ToolTipTarget; ToolTip.Body = ToolTipBody; ToolTip.peek = { description: `A utility component that creates a transient message anchored to another component.`, notes: { overview: `A text popup shown on hover.`, intendedUse: ` Use to provide an explanation for a button, text, or an operation. Often used in conjunction with \`HelpIcon\`. **Styling notes** - Use the {direction} and {alignment} that best suit your layout. - Tooltip should typically not use a Title. If one does, it should fit on a single line and not wrap. - Use black tooltips in most interactions. White tooltips are reserved for use within charts, for example \`LineChart\`. `, technicalRecommendations: ` `, }, categories: ['communication'], madeFrom: ['ContextMenu'], }; ToolTip.reducers = reducers; ToolTip.propTypes = { /** * \`children\` should include exactly one ToolTip.Target and one ToolTip.FlyOut. */ children: node, /** * Appended to the component-specific class names set on the root element. */ className: string, /** * Set this to \`true\` if you want to have a \`x\` close icon. */ isCloseable: bool, /** * Offers a lighter style for the tooltip window. Defaults to false. */ isLight: bool, /** * Called when the user closes the \`Banner\`. * Signature: \`({ event, props }) => {}\` */ onClose: func, /** Passed through to the root target element.*/ style: object, /** Passed through to the root FlyOut element.*/ flyOutStyle: object, /** * maximum width of the ToolTip FlyOut. Defaults to 200px. */ flyOutMaxWidth: oneOfType([number, string]), /** * direction of the FlyOut relative to Target. */ direction: oneOf(['down', 'up', 'right', 'left']), /** * alignment of the Flyout relative to Target in the cross axis from \`direction\`. */ alignment: oneOf(['start', 'center', 'end']), /** * Indicates whether the ToolTip will render or not. */ isExpanded: bool, /** * Called when cursor moves over the target * Signature: \`({ props, event }) => {}\` */ onMouseOver: func, /** * Called when cursor leaves the target and the ToolTip * Signature: \`({ props, event }) => {}\` */ onMouseOut: func, /** * The \`id\` of the FlyOut portal element that is appended to \`document.body\`. * Defaults to a generated \`id\`. */ portalId: string, /** * Tooltips do not typically have a Title but one can be displayed above the Body. */ Title: node, /** * The body of the 'ToolTip'. */ Body: node, /** * The hover target that will trigger the ToolTip to be displayed. */ Target: node, }; ToolTip.defaultProps = { alignment: ContextMenu.CENTER, direction: ContextMenu.UP, flyOutStyle: {}, isCloseable: false, isExpanded: false, isLight: false, onClose: _.noop, onMouseOut: _.noop, onMouseOver: _.noop, portalId: null, }; export default buildModernHybridComponent(ToolTip, { reducers }); export { ToolTip as ToolTipDumb }; //# sourceMappingURL=ToolTip.js.map