From 7699b96c513d3d599c87ac4409f5e652b26c7a45 Mon Sep 17 00:00:00 2001 From: MichelNogales Date: Mon, 29 May 2023 14:23:47 -0600 Subject: [PATCH] fix: add tooltip to button --- package-lock.json | 14 + package.json | 1 + src/components/Button.tsx | 111 ++++--- src/components/Fade.js | 82 +++++ src/components/PopperContent.js | 238 +++++++++++++ src/components/Tooltip2.tsx | 42 +++ src/components/TooltipPopoverWrapper.js | 423 ++++++++++++++++++++++++ src/components/index.tsx | 4 +- src/components/utils.js | 383 +++++++++++++++++++++ 9 files changed, 1257 insertions(+), 41 deletions(-) create mode 100644 src/components/Fade.js create mode 100644 src/components/PopperContent.js create mode 100644 src/components/Tooltip2.tsx create mode 100644 src/components/TooltipPopoverWrapper.js create mode 100644 src/components/utils.js diff --git a/package-lock.json b/package-lock.json index 14097e2..2d75d42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29190,6 +29190,11 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, "react-full-screen": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/react-full-screen/-/react-full-screen-1.1.1.tgz", @@ -29219,6 +29224,15 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "requires": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", diff --git a/package.json b/package.json index 400e8ef..a548e12 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "prop-types": "^15.8.1", "prop-types-extra": "^1.1.0", "react-full-screen": "^1.1.1", + "react-popper": "^2.3.0", "react-select": "^5.7.0", "react-spinners": "^0.13.8", "react-transition-group": "^4.4.2", diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 3c052a5..ec1b6aa 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -10,7 +10,7 @@ import { useBootstrapPrefix } from './ThemeProvider'; import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers'; import { ButtonVariant } from './types'; import OverlayTrigger from './OverlayTrigger'; -import Tooltip from './Tooltip'; +import { Tooltip2 } from './Tooltip2'; import { v4 as uuid } from 'uuid'; const faAsterisk = { @@ -126,6 +126,26 @@ const defaultProps = { animateIconClass: false, }; +const useTooltip = (tooltip: any) => { + if (!tooltip) { + return { + tooltipOpen: null, + setTooltipOpen: null, + toggle: null, + tooltipId: null, + }; + } + const [tooltipOpen, setTooltipOpen] = React.useState(false); + const toggle = () => setTooltipOpen(!tooltipOpen); + const [tooltipId] = React.useState(`button-tooltip-${uuid()}`); + + return { + tooltipOpen, + setTooltipOpen, + toggle, + tooltipId, + }; +}; export const Button: BsPrefixRefForwardingComponent<'button', ButtonProps> = React.forwardRef( ( @@ -150,6 +170,9 @@ export const Button: BsPrefixRefForwardingComponent<'button', ButtonProps> = }, ref ) => { + const { tooltipOpen, setTooltipOpen, toggle, tooltipId } = + useTooltip(tooltip); + const prefix = useBootstrapPrefix(bsPrefix, 'btn'); const [buttonProps, { tagName }] = useButtonProps({ tagName: as, @@ -205,47 +228,55 @@ export const Button: BsPrefixRefForwardingComponent<'button', ButtonProps> = ); }; - const renderTooltip = () => ( - {tooltip} - ); + return ( + + + {iconRight && ( + + {label ? label : children}{' '} + {icon && getIcon(icon)} + + )} + {!iconRight && ( + + {icon && getIcon(icon)}{' '} + {label ? label : children} + + )} + - const RenderButton = () => ( - - {iconRight && ( - - {label ? label : children} {icon && getIcon(icon)} - - )} - {!iconRight && ( - - {icon && getIcon(icon)} {label ? label : children} - + {tooltip && tooltipId && ( + // @ts-ignore + +

{tooltip}

+
)} -
- ); - - if (!tooltip) { - return ; - } - - return ( - - - +
); } ); diff --git a/src/components/Fade.js b/src/components/Fade.js new file mode 100644 index 0000000..23f3df8 --- /dev/null +++ b/src/components/Fade.js @@ -0,0 +1,82 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Transition } from 'react-transition-group'; +import { + mapToCssModules, + omit, + pick, + TransitionPropTypeKeys, + TransitionTimeouts, + tagPropType, +} from './utils'; + +const propTypes = { + ...Transition.propTypes, + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + tag: tagPropType, + baseClass: PropTypes.string, + baseClassActive: PropTypes.string, + className: PropTypes.string, + cssModule: PropTypes.object, + innerRef: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.string, + PropTypes.func, + ]), +}; + +const defaultProps = { + ...Transition.defaultProps, + tag: 'div', + baseClass: 'fade', + baseClassActive: 'show', + timeout: TransitionTimeouts.Fade, + appear: true, + enter: true, + exit: true, + in: true, +}; + +function Fade(props) { + const ref = useRef(null); + + const { + tag: Tag, + baseClass, + baseClassActive, + className, + cssModule, + children, + innerRef = ref, + ...otherProps + } = props; + + const transitionProps = pick(otherProps, TransitionPropTypeKeys); + const childProps = omit(otherProps, TransitionPropTypeKeys); + + return ( + + {(status) => { + const isActive = status === 'entered'; + const classes = mapToCssModules( + classNames(className, baseClass, isActive && baseClassActive), + cssModule, + ); + return ( + + {children} + + ); + }} + + ); +} + +Fade.propTypes = propTypes; +Fade.defaultProps = defaultProps; + +export default Fade; \ No newline at end of file diff --git a/src/components/PopperContent.js b/src/components/PopperContent.js new file mode 100644 index 0000000..bdda564 --- /dev/null +++ b/src/components/PopperContent.js @@ -0,0 +1,238 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import { Popper as ReactPopper } from 'react-popper'; +import { + getTarget, + targetPropType, + mapToCssModules, + DOMElement, + tagPropType, +} from './utils'; +import Fade from './Fade'; + +function noop() {} + +const propTypes = { + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, + popperClassName: PropTypes.string, + placement: PropTypes.string, + placementPrefix: PropTypes.string, + arrowClassName: PropTypes.string, + hideArrow: PropTypes.bool, + tag: tagPropType, + isOpen: PropTypes.bool, + cssModule: PropTypes.object, + offset: PropTypes.arrayOf(PropTypes.number), + fallbackPlacements: PropTypes.array, + flip: PropTypes.bool, + container: targetPropType, + target: targetPropType.isRequired, + modifiers: PropTypes.array, + strategy: PropTypes.string, + boundariesElement: PropTypes.oneOfType([PropTypes.string, DOMElement]), + onClosed: PropTypes.func, + fade: PropTypes.bool, + transition: PropTypes.shape(Fade.propTypes), +}; + +const defaultProps = { + boundariesElement: 'scrollParent', + placement: 'auto', + hideArrow: false, + isOpen: false, + offset: [0, 0], + flip: true, + container: 'body', + modifiers: [], + onClosed: noop, + fade: true, + transition: { + ...Fade.defaultProps, + }, +}; + +class PopperContent extends React.Component { + constructor(props) { + super(props); + + this.setTargetNode = this.setTargetNode.bind(this); + this.getTargetNode = this.getTargetNode.bind(this); + this.getRef = this.getRef.bind(this); + this.onClosed = this.onClosed.bind(this); + this.state = { isOpen: props.isOpen }; + } + + static getDerivedStateFromProps(props, state) { + if (props.isOpen && !state.isOpen) { + return { isOpen: props.isOpen }; + } + return null; + } + + componentDidUpdate() { + if ( + this._element && + this._element.childNodes && + this._element.childNodes[0] && + this._element.childNodes[0].focus + ) { + this._element.childNodes[0].focus(); + } + } + + onClosed() { + this.props.onClosed(); + this.setState({ isOpen: false }); + } + + getTargetNode() { + return this.targetNode; + } + + getContainerNode() { + return getTarget(this.props.container); + } + + getRef(ref) { + this._element = ref; + } + + setTargetNode(node) { + this.targetNode = typeof node === 'string' ? getTarget(node) : node; + } + + renderChildren() { + const { + cssModule, + children, + isOpen, + flip, + target, + offset, + fallbackPlacements, + placementPrefix, + arrowClassName: _arrowClassName, + hideArrow, + popperClassName: _popperClassName, + tag, + container, + modifiers, + strategy, + boundariesElement, + onClosed, + fade, + transition, + placement, + ...attrs + } = this.props; + const arrowClassName = mapToCssModules( + classNames('arrow', _arrowClassName), + cssModule, + ); + const popperClassName = mapToCssModules( + classNames( + _popperClassName, + placementPrefix ? `${placementPrefix}-auto` : '', + ), + this.props.cssModule, + ); + + const modifierNames = modifiers.map((m) => m.name); + const baseModifiers = [ + { + name: 'offset', + options: { + offset, + }, + }, + { + name: 'flip', + enabled: flip, + options: { + fallbackPlacements, + }, + }, + { + name: 'preventOverflow', + options: { + boundary: boundariesElement, + }, + }, + ].filter((m) => !modifierNames.includes(m.name)); + const extendedModifiers = [...baseModifiers, ...modifiers]; + + const popperTransition = { + ...Fade.defaultProps, + ...transition, + baseClass: fade ? transition.baseClass : '', + timeout: fade ? transition.timeout : 0, + }; + + return ( + + + {({ + ref, + style, + placement: popperPlacement, + isReferenceHidden, + arrowProps, + update, + }) => ( +
+ {typeof children === 'function' ? children({ update }) : children} + {!hideArrow && ( + + )} +
+ )} +
+
+ ); + } + + render() { + this.setTargetNode(this.props.target); + + if (this.state.isOpen) { + return this.props.container === 'inline' + ? this.renderChildren() + : ReactDOM.createPortal( +
{this.renderChildren()}
, + this.getContainerNode(), + ); + } + + return null; + } +} + +PopperContent.propTypes = propTypes; +PopperContent.defaultProps = defaultProps; + +export default PopperContent; \ No newline at end of file diff --git a/src/components/Tooltip2.tsx b/src/components/Tooltip2.tsx new file mode 100644 index 0000000..5cef446 --- /dev/null +++ b/src/components/Tooltip2.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import classNames from 'classnames'; +import TooltipPopoverWrapper, { propTypes } from './TooltipPopoverWrapper'; + +export interface ITooltipProps { + placement?: string; + autohide?: boolean; + placementPrefix?: string; + trigger?: string; + popperClassName?: any; + innerClassName?: any; +} + +export const Tooltip2: React.FC = ({ + placement = 'top', + autohide = true, + placementPrefix = 'bs-tooltip', + trigger = 'hover focus', + ...propsMain +}) => { + const props = { + placement, + autohide, + placementPrefix, + trigger, + ...propsMain, + }; + const popperClasses = classNames('tooltip', 'show', props.popperClassName); + + const classes = classNames('tooltip-inner', props.innerClassName); + + return ( + + ); +}; + +export default Tooltip2; diff --git a/src/components/TooltipPopoverWrapper.js b/src/components/TooltipPopoverWrapper.js new file mode 100644 index 0000000..6520524 --- /dev/null +++ b/src/components/TooltipPopoverWrapper.js @@ -0,0 +1,423 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import PopperContent from './PopperContent'; +import { + getTarget, + targetPropType, + omit, + PopperPlacements, + mapToCssModules, + DOMElement, +} from './utils'; + +export const propTypes = { + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + placement: PropTypes.oneOf(PopperPlacements), + target: targetPropType.isRequired, + container: targetPropType, + isOpen: PropTypes.bool, + disabled: PropTypes.bool, + hideArrow: PropTypes.bool, + boundariesElement: PropTypes.oneOfType([PropTypes.string, DOMElement]), + className: PropTypes.string, + innerClassName: PropTypes.string, + arrowClassName: PropTypes.string, + popperClassName: PropTypes.string, + cssModule: PropTypes.object, + toggle: PropTypes.func, + autohide: PropTypes.bool, + placementPrefix: PropTypes.string, + delay: PropTypes.oneOfType([ + PropTypes.shape({ show: PropTypes.number, hide: PropTypes.number }), + PropTypes.number, + ]), + modifiers: PropTypes.array, + strategy: PropTypes.string, + offset: PropTypes.arrayOf(PropTypes.number), + innerRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + PropTypes.object, + ]), + trigger: PropTypes.string, + fade: PropTypes.bool, + flip: PropTypes.bool, +}; + +const DEFAULT_DELAYS = { + show: 0, + hide: 50, +}; + +const defaultProps = { + isOpen: false, + hideArrow: false, + autohide: false, + delay: DEFAULT_DELAYS, + toggle: function () {}, + trigger: 'click', + fade: true, +}; + +function isInDOMSubtree(element, subtreeRoot) { + return ( + subtreeRoot && + (element === subtreeRoot || subtreeRoot.contains(element)) + ); +} + +function isInDOMSubtrees(element, subtreeRoots = []) { + return ( + subtreeRoots && + subtreeRoots.length && + subtreeRoots.filter((subTreeRoot) => + isInDOMSubtree(element, subTreeRoot) + )[0] + ); +} + +class TooltipPopoverWrapper extends React.Component { + constructor(props) { + super(props); + + this._targets = []; + this.currentTargetElement = null; + this.addTargetEvents = this.addTargetEvents.bind(this); + this.handleDocumentClick = this.handleDocumentClick.bind(this); + this.removeTargetEvents = this.removeTargetEvents.bind(this); + this.toggle = this.toggle.bind(this); + this.showWithDelay = this.showWithDelay.bind(this); + this.hideWithDelay = this.hideWithDelay.bind(this); + this.onMouseOverTooltipContent = + this.onMouseOverTooltipContent.bind(this); + this.onMouseLeaveTooltipContent = + this.onMouseLeaveTooltipContent.bind(this); + this.show = this.show.bind(this); + this.hide = this.hide.bind(this); + this.onEscKeyDown = this.onEscKeyDown.bind(this); + this.getRef = this.getRef.bind(this); + this.state = { isOpen: props.isOpen }; + this._isMounted = false; + } + + componentDidMount() { + this._isMounted = true; + this.updateTarget(); + } + + componentWillUnmount() { + this._isMounted = false; + this.removeTargetEvents(); + this._targets = null; + this.clearShowTimeout(); + this.clearHideTimeout(); + } + + static getDerivedStateFromProps(props, state) { + if (props.isOpen && !state.isOpen) { + return { isOpen: props.isOpen }; + } + return null; + } + + handleDocumentClick(e) { + const triggers = this.props.trigger.split(' '); + + if ( + triggers.indexOf('legacy') > -1 && + (this.props.isOpen || isInDOMSubtrees(e.target, this._targets)) + ) { + if (this._hideTimeout) { + this.clearHideTimeout(); + } + if (this.props.isOpen && !isInDOMSubtree(e.target, this._popover)) { + this.hideWithDelay(e); + } else if (!this.props.isOpen) { + this.showWithDelay(e); + } + } else if ( + triggers.indexOf('click') > -1 && + isInDOMSubtrees(e.target, this._targets) + ) { + if (this._hideTimeout) { + this.clearHideTimeout(); + } + + if (!this.props.isOpen) { + this.showWithDelay(e); + } else { + this.hideWithDelay(e); + } + } + } + + onMouseOverTooltipContent() { + if (this.props.trigger.indexOf('hover') > -1 && !this.props.autohide) { + if (this._hideTimeout) { + this.clearHideTimeout(); + } + if (this.state.isOpen && !this.props.isOpen) { + this.toggle(); + } + } + } + + onMouseLeaveTooltipContent(e) { + if (this.props.trigger.indexOf('hover') > -1 && !this.props.autohide) { + if (this._showTimeout) { + this.clearShowTimeout(); + } + e.persist(); + this._hideTimeout = setTimeout( + this.hide.bind(this, e), + this.getDelay('hide') + ); + } + } + + onEscKeyDown(e) { + if (e.key === 'Escape') { + this.hide(e); + } + } + + getRef(ref) { + const { innerRef } = this.props; + if (innerRef) { + if (typeof innerRef === 'function') { + innerRef(ref); + } else if (typeof innerRef === 'object') { + innerRef.current = ref; + } + } + this._popover = ref; + } + + getDelay(key) { + const { delay } = this.props; + if (typeof delay === 'object') { + return isNaN(delay[key]) ? DEFAULT_DELAYS[key] : delay[key]; + } + return delay; + } + + getCurrentTarget(target) { + if (!target) return null; + const index = this._targets.indexOf(target); + if (index >= 0) return this._targets[index]; + return this.getCurrentTarget(target.parentElement); + } + + show(e) { + if (!this.props.isOpen) { + this.clearShowTimeout(); + this.currentTargetElement = e + ? e.currentTarget || this.getCurrentTarget(e.target) + : null; + if (e && e.composedPath && typeof e.composedPath === 'function') { + const path = e.composedPath(); + this.currentTargetElement = + (path && path[0]) || this.currentTargetElement; + } + this.toggle(e); + } + } + + showWithDelay(e) { + if (this._hideTimeout) { + this.clearHideTimeout(); + } + this._showTimeout = setTimeout( + this.show.bind(this, e), + this.getDelay('show') + ); + } + + hide(e) { + if (this.props.isOpen) { + this.clearHideTimeout(); + this.currentTargetElement = null; + this.toggle(e); + } + } + + hideWithDelay(e) { + if (this._showTimeout) { + this.clearShowTimeout(); + } + this._hideTimeout = setTimeout( + this.hide.bind(this, e), + this.getDelay('hide') + ); + } + + clearShowTimeout() { + clearTimeout(this._showTimeout); + this._showTimeout = undefined; + } + + clearHideTimeout() { + clearTimeout(this._hideTimeout); + this._hideTimeout = undefined; + } + + addEventOnTargets(type, handler, isBubble) { + this._targets.forEach((target) => { + target.addEventListener(type, handler, isBubble); + }); + } + + removeEventOnTargets(type, handler, isBubble) { + this._targets.forEach((target) => { + target.removeEventListener(type, handler, isBubble); + }); + } + + addTargetEvents() { + if (this.props.trigger) { + let triggers = this.props.trigger.split(' '); + if (triggers.indexOf('manual') === -1) { + if ( + triggers.indexOf('click') > -1 || + triggers.indexOf('legacy') > -1 + ) { + document.addEventListener( + 'click', + this.handleDocumentClick, + true + ); + } + + if (this._targets && this._targets.length) { + if (triggers.indexOf('hover') > -1) { + this.addEventOnTargets( + 'mouseover', + this.showWithDelay, + true + ); + this.addEventOnTargets( + 'mouseout', + this.hideWithDelay, + true + ); + } + if (triggers.indexOf('focus') > -1) { + this.addEventOnTargets('focusin', this.show, true); + this.addEventOnTargets('focusout', this.hide, true); + } + this.addEventOnTargets('keydown', this.onEscKeyDown, true); + } + } + } + } + + removeTargetEvents() { + if (this._targets) { + this.removeEventOnTargets('mouseover', this.showWithDelay, true); + this.removeEventOnTargets('mouseout', this.hideWithDelay, true); + this.removeEventOnTargets('keydown', this.onEscKeyDown, true); + this.removeEventOnTargets('focusin', this.show, true); + this.removeEventOnTargets('focusout', this.hide, true); + } + + document.removeEventListener('click', this.handleDocumentClick, true); + } + + updateTarget() { + const newTarget = getTarget(this.props.target, true); + if (newTarget !== this._targets) { + this.removeTargetEvents(); + this._targets = newTarget ? Array.from(newTarget) : []; + this.currentTargetElement = + this.currentTargetElement || this._targets[0]; + this.addTargetEvents(); + } + } + + toggle(e) { + if (this.props.disabled || !this._isMounted) { + return e && e.preventDefault(); + } + + return this.props.toggle(e); + } + + render() { + if (this.props.isOpen) { + this.updateTarget(); + } + + const target = this.currentTargetElement || this._targets[0]; + if (!target) { + return null; + } + + const { + className, + cssModule, + innerClassName, + isOpen, + hideArrow, + boundariesElement, + placement, + placementPrefix, + arrowClassName, + popperClassName, + container, + modifiers, + strategy, + offset, + fade, + flip, + children, + } = this.props; + + const attributes = omit(this.props, Object.keys(propTypes)); + + const popperClasses = mapToCssModules(popperClassName, cssModule); + + const classes = mapToCssModules(innerClassName, cssModule); + + return ( + + {({ update }) => ( +
+ {typeof children === 'function' + ? children({ update }) + : children} +
+ )} +
+ ); + } +} + +TooltipPopoverWrapper.propTypes = propTypes; +TooltipPopoverWrapper.defaultProps = defaultProps; + +export default TooltipPopoverWrapper; diff --git a/src/components/index.tsx b/src/components/index.tsx index eeb057c..43117eb 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -254,6 +254,8 @@ export type { FileUploadS3MultipartProps } from './FileUploadS3Multipart'; export { default as useFileDownloadS3Multipart } from './useFileDownloadS3Multipart'; export type { FileDownloadS3MultipartProps } from './useFileDownloadS3Multipart'; - export { default as useFileUploadS3Multipart } from './useFileUploadS3Multipart'; export type { useFileUploadS3MultipartProps } from './useFileUploadS3Multipart'; + +export { default as Tooltip2 } from './Tooltip2'; +export type { ITooltipProps } from './Tooltip2'; diff --git a/src/components/utils.js b/src/components/utils.js new file mode 100644 index 0000000..c73da70 --- /dev/null +++ b/src/components/utils.js @@ -0,0 +1,383 @@ +import PropTypes from 'prop-types'; + +// https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.4/js/src/modal.js#L436-L443 +export function getScrollbarWidth() { + let scrollDiv = document.createElement('div'); + // .modal-scrollbar-measure styles // https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.4/scss/_modal.scss#L106-L113 + scrollDiv.style.position = 'absolute'; + scrollDiv.style.top = '-9999px'; + scrollDiv.style.width = '50px'; + scrollDiv.style.height = '50px'; + scrollDiv.style.overflow = 'scroll'; + document.body.appendChild(scrollDiv); + const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + return scrollbarWidth; +} + +export function setScrollbarWidth(padding) { + document.body.style.paddingRight = padding > 0 ? `${padding}px` : null; +} + +export function isBodyOverflowing() { + return document.body.clientWidth < window.innerWidth; +} + +export function getOriginalBodyPadding() { + const style = window.getComputedStyle(document.body, null); + + return parseInt((style && style.getPropertyValue('padding-right')) || 0, 10); +} + +export function conditionallyUpdateScrollbar() { + const scrollbarWidth = getScrollbarWidth(); + // https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.6/js/src/modal.js#L433 + const fixedContent = document.querySelectorAll( + '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', + )[0]; + const bodyPadding = fixedContent + ? parseInt(fixedContent.style.paddingRight || 0, 10) + : 0; + + if (isBodyOverflowing()) { + setScrollbarWidth(bodyPadding + scrollbarWidth); + } +} + +let globalCssModule; + +export function setGlobalCssModule(cssModule) { + globalCssModule = cssModule; +} + +export function mapToCssModules(className = '', cssModule = globalCssModule) { + if (!cssModule) return className; + return className + .split(' ') + .map((c) => cssModule[c] || c) + .join(' '); +} + +/** + * Returns a new object with the key/value pairs from `obj` that are not in the array `omitKeys`. + */ +export function omit(obj, omitKeys) { + const result = {}; + Object.keys(obj).forEach((key) => { + if (omitKeys.indexOf(key) === -1) { + result[key] = obj[key]; + } + }); + return result; +} + +/** + * Returns a filtered copy of an object with only the specified keys. + */ +export function pick(obj, keys) { + const pickKeys = Array.isArray(keys) ? keys : [keys]; + let { length } = pickKeys; + let key; + const result = {}; + + while (length > 0) { + length -= 1; + key = pickKeys[length]; + result[key] = obj[key]; + } + return result; +} + +let warned = {}; + +export function warnOnce(message) { + if (!warned[message]) { + /* istanbul ignore else */ + if (typeof console !== 'undefined') { + console.error(message); // eslint-disable-line no-console + } + warned[message] = true; + } +} + +export function deprecated(propType, explanation) { + return function validate(props, propName, componentName, ...rest) { + if (props[propName] !== null && typeof props[propName] !== 'undefined') { + warnOnce( + `"${propName}" property of "${componentName}" has been deprecated.\n${explanation}`, + ); + } + + return propType(props, propName, componentName, ...rest); + }; +} + +// Shim Element if needed (e.g. in Node environment) +const Element = + (typeof window === 'object' && window.Element) || function () {}; + +export function DOMElement(props, propName, componentName) { + if (!(props[propName] instanceof Element)) { + return new Error( + 'Invalid prop `' + + propName + + '` supplied to `' + + componentName + + '`. Expected prop to be an instance of Element. Validation failed.', + ); + } +} + +export const targetPropType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func, + DOMElement, + PropTypes.shape({ current: PropTypes.any }), +]); + +export const tagPropType = PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + PropTypes.shape({ $$typeof: PropTypes.symbol, render: PropTypes.func }), + PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + PropTypes.shape({ $$typeof: PropTypes.symbol, render: PropTypes.func }), + ]), + ), +]); + +// These are all setup to match what is in the bootstrap _variables.scss +// https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss +export const TransitionTimeouts = { + Fade: 150, // $transition-fade + Collapse: 350, // $transition-collapse + Modal: 300, // $modal-transition + Carousel: 600, // $carousel-transition + Offcanvas: 300, // $offcanvas-transition +}; + +// Duplicated Transition.propType keys to ensure that Reactstrap builds +// for distribution properly exclude these keys for nested child HTML attributes +// since `react-transition-group` removes propTypes in production builds. +export const TransitionPropTypeKeys = [ + 'in', + 'mountOnEnter', + 'unmountOnExit', + 'appear', + 'enter', + 'exit', + 'timeout', + 'onEnter', + 'onEntering', + 'onEntered', + 'onExit', + 'onExiting', + 'onExited', +]; + +export const TransitionStatuses = { + ENTERING: 'entering', + ENTERED: 'entered', + EXITING: 'exiting', + EXITED: 'exited', +}; + +export const keyCodes = { + esc: 27, + space: 32, + enter: 13, + tab: 9, + up: 38, + down: 40, + home: 36, + end: 35, + n: 78, + p: 80, +}; + +export const PopperPlacements = [ + 'auto-start', + 'auto', + 'auto-end', + 'top-start', + 'top', + 'top-end', + 'right-start', + 'right', + 'right-end', + 'bottom-end', + 'bottom', + 'bottom-start', + 'left-end', + 'left', + 'left-start', +]; + +export const canUseDOM = !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement +); + +export function isReactRefObj(target) { + if (target && typeof target === 'object') { + return 'current' in target; + } + return false; +} + +function getTag(value) { + if (value == null) { + return value === undefined ? '[object Undefined]' : '[object Null]'; + } + return Object.prototype.toString.call(value); +} + +export function isObject(value) { + const type = typeof value; + return value != null && (type === 'object' || type === 'function'); +} + +export function toNumber(value) { + const type = typeof value; + const NAN = 0 / 0; + if (type === 'number') { + return value; + } + if ( + type === 'symbol' || + (type === 'object' && getTag(value) === '[object Symbol]') + ) { + return NAN; + } + if (isObject(value)) { + const other = typeof value.valueOf === 'function' ? value.valueOf() : value; + value = isObject(other) ? `${other}` : other; + } + if (type !== 'string') { + return value === 0 ? value : +value; + } + value = value.replace(/^\s+|\s+$/g, ''); + const isBinary = /^0b[01]+$/i.test(value); + return isBinary || /^0o[0-7]+$/i.test(value) + ? parseInt(value.slice(2), isBinary ? 2 : 8) + : /^[-+]0x[0-9a-f]+$/i.test(value) + ? NAN + : +value; +} + +export function isFunction(value) { + if (!isObject(value)) { + return false; + } + + const tag = getTag(value); + return ( + tag === '[object Function]' || + tag === '[object AsyncFunction]' || + tag === '[object GeneratorFunction]' || + tag === '[object Proxy]' + ); +} + +export function findDOMElements(target) { + if (isReactRefObj(target)) { + return target.current; + } + if (isFunction(target)) { + return target(); + } + if (typeof target === 'string' && canUseDOM) { + let selection = document.querySelectorAll(target); + if (!selection.length) { + selection = document.querySelectorAll(`#${target}`); + } + if (!selection.length) { + throw new Error( + `The target '${target}' could not be identified in the dom, tip: check spelling`, + ); + } + return selection; + } + return target; +} + +export function isArrayOrNodeList(els) { + if (els === null) { + return false; + } + return Array.isArray(els) || (canUseDOM && typeof els.length === 'number'); +} + +export function getTarget(target, allElements) { + const els = findDOMElements(target); + if (allElements) { + if (isArrayOrNodeList(els)) { + return els; + } + if (els === null) { + return []; + } + return [els]; + } + if (isArrayOrNodeList(els)) { + return els[0]; + } + return els; +} + +export const defaultToggleEvents = ['touchstart', 'click']; + +export function addMultipleEventListeners(_els, handler, _events, useCapture) { + let els = _els; + if (!isArrayOrNodeList(els)) { + els = [els]; + } + + let events = _events; + if (typeof events === 'string') { + events = events.split(/\s+/); + } + + if ( + !isArrayOrNodeList(els) || + typeof handler !== 'function' || + !Array.isArray(events) + ) { + throw new Error(` + The first argument of this function must be DOM node or an array on DOM nodes or NodeList. + The second must be a function. + The third is a string or an array of strings that represents DOM events + `); + } + + Array.prototype.forEach.call(events, (event) => { + Array.prototype.forEach.call(els, (el) => { + el.addEventListener(event, handler, useCapture); + }); + }); + return function removeEvents() { + Array.prototype.forEach.call(events, (event) => { + Array.prototype.forEach.call(els, (el) => { + el.removeEventListener(event, handler, useCapture); + }); + }); + }; +} + +export const focusableElements = [ + 'a[href]', + 'area[href]', + 'input:not([disabled]):not([type=hidden])', + 'select:not([disabled])', + 'textarea:not([disabled])', + 'button:not([disabled])', + 'object', + 'embed', + '[tabindex]:not(.modal):not(.offcanvas)', + 'audio[controls]', + 'video[controls]', + '[contenteditable]:not([contenteditable="false"])', +]; \ No newline at end of file