/* @flow */ import InlineDialog from '@atlaskit/inline-dialog'; import React, { Component } from 'react'; import { Drawer, DrawerPortal } from '../../../toolbox/components/web'; /** * A map of dialog positions, relative to trigger, to css classes used to * manipulate elements for handling mouse events. * * @private * @type {object} */ const DIALOG_TO_PADDING_POSITION = { 'left': 'popover-mousemove-padding-right', 'right': 'popover-mousemove-padding-left', 'top': 'popover-mousemove-padding-bottom' }; /** * Takes the position expected by {@code InlineDialog} and maps it to a CSS * class that can be used styling the elements used for preventing mouseleave * events when moving from the trigger to the dialog. * * @param {string} position - From which position the dialog will display. * @private * @returns {string} */ function _mapPositionToPaddingClass(position = 'left') { return DIALOG_TO_PADDING_POSITION[position.split('-')[0]]; } /** * The type of the React {@code Component} props of {@link Popover}. */ type Props = { /** * A child React Element to use as the trigger for showing the dialog. */ children: React$Node, /** * Additional CSS classnames to apply to the root of the {@code Popover} * component. */ className: string, /** * The ReactElement to display within the dialog. */ content: Object, /** * Whether displaying of the popover should be prevented. */ disablePopover: boolean, /** * An id attribute to apply to the root of the {@code Popover} * component. */ id: string, /** * Callback to invoke when the popover has closed. */ onPopoverClose: Function, /** * Callback to invoke when the popover has opened. */ onPopoverOpen: Function, /** * Whether to display the Popover as a drawer. */ overflowDrawer: boolean, /** * From which side of the dialog trigger the dialog should display. The * value will be passed to {@code InlineDialog}. */ position: string }; /** * The type of the React {@code Component} state of {@link Popover}. */ type State = { /** * Whether or not the {@code InlineDialog} should be displayed. */ showDialog: boolean }; /** * Implements a React {@code Component} for showing an {@code InlineDialog} on * mouseenter of the trigger and contents, and hiding the dialog on mouseleave. * * @extends Component */ class Popover extends Component { /** * Default values for {@code Popover} component's properties. * * @static */ static defaultProps = { className: '', id: '' }; /** * Reference to the dialog container. */ _containerRef: Object; /** * Initializes a new {@code Popover} instance. * * @param {Object} props - The read-only properties with which the new * instance is to be initialized. */ constructor(props: Props) { super(props); this.state = { showDialog: false }; // Bind event handlers so they are only bound once for every instance. this._onHideDialog = this._onHideDialog.bind(this); this._onShowDialog = this._onShowDialog.bind(this); this._onKeyPress = this._onKeyPress.bind(this); this._containerRef = React.createRef(); this._onEscKey = this._onEscKey.bind(this); this._onThumbClick = this._onThumbClick.bind(this); this._onTouchStart = this._onTouchStart.bind(this); } /** * Public method for triggering showing the context menu dialog. * * @returns {void} * @public */ showDialog() { this._onShowDialog(); } /** * Sets up a touch event listener to attach. * * @inheritdoc * @returns {void} */ componentDidMount() { window.addEventListener('touchstart', this._onTouchStart); } /** * Removes the listener set up in the {@code componentDidMount} method. * * @inheritdoc * @returns {void} */ componentWillUnmount() { window.removeEventListener('touchstart', this._onTouchStart); } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ render() { const { children, className, content, id, overflowDrawer, position } = this.props; if (overflowDrawer) { return (
{ children } { content }
); } return (
{ children }
); } _onTouchStart: (event: TouchEvent) => void; /** * Hide dialog on touch outside of the context menu. * * @param {TouchEvent} event - The touch event. * @private * @returns {void} */ _onTouchStart(event) { if (this.state.showDialog && !this.props.overflowDrawer && this._containerRef && this._containerRef.current && !this._containerRef.current.contains(event.target)) { this._onHideDialog(); } } _onHideDialog: () => void; /** * Stops displaying the {@code InlineDialog}. * * @private * @returns {void} */ _onHideDialog() { this.setState({ showDialog: false }); if (this.props.onPopoverClose) { this.props.onPopoverClose(); } } _onShowDialog: (Object) => void; /** * Displays the {@code InlineDialog} and calls any registered onPopoverOpen * callbacks. * * @param {Object} event - The mouse event or the keypress event to intercept. * @private * @returns {void} */ _onShowDialog(event) { event && event.stopPropagation(); if (!this.props.disablePopover) { this.setState({ showDialog: true }); if (this.props.onPopoverOpen) { this.props.onPopoverOpen(); } } } _onThumbClick: (Object) => void; /** * Prevents switching from tile view to stage view on accidentally clicking * the popover thumbs. * * @param {Object} event - The mouse event or the keypress event to intercept. * @private * @returns {void} */ _onThumbClick(event) { event.stopPropagation(); } _onKeyPress: (Object) => void; /** * KeyPress handler for accessibility. * * @param {Object} e - The key event to handle. * * @returns {void} */ _onKeyPress(e) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); if (this.state.showDialog) { this._onHideDialog(); } else { this._onShowDialog(e); } } } _onEscKey: (Object) => void; /** * KeyPress handler for accessibility. * * @param {Object} e - The key event to handle. * * @returns {void} */ _onEscKey(e) { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); if (this.state.showDialog) { this._onHideDialog(); } } } /** * Renders the React Element to be displayed in the {@code InlineDialog}. * Also adds padding to support moving the mouse from the trigger to the * dialog to prevent mouseleave events. * * @private * @returns {ReactElement} */ _renderContent() { const { content, position } = this.props; return (
{ content }
); } } export default Popover;