UNPKG

@atlaskit/editor-plugin-floating-toolbar

Version:

Floating toolbar plugin for @atlaskit/editor-core

147 lines (145 loc) 5.97 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; /** * @jsxRuntime classic * @jsx jsx */ import React, { useCallback, useContext } from 'react'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 import { css, jsx } from '@emotion/react'; import { IconButton } from '@atlaskit/button/new'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { Popup } from '@atlaskit/editor-common/ui'; import { OutsideClickTargetRefContext, withReactEditorViewOuterListeners } from '@atlaskit/editor-common/ui-react'; import { EmojiPicker } from '@atlaskit/emoji'; import EmojiAddIcon from '@atlaskit/icon/core/emoji-add'; import Tooltip from '@atlaskit/tooltip'; // eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage var emojiPickerButtonWrapperVisualRefresh = css({ position: 'relative', // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors button: { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-selectors '&:not([disabled])::after': { border: 'none' // remove blue border when picker has been selected } } }); var selector = function selector(states) { var _states$emojiState; return (_states$emojiState = states.emojiState) === null || _states$emojiState === void 0 ? void 0 : _states$emojiState.emojiProvider; }; var EmojiPickerWithProvider = function EmojiPickerWithProvider(props) { var emojiProvider = useSharedPluginStateWithSelector(props.pluginInjectionApi, ['emoji'], selector); var setOutsideClickTargetRef = useContext(OutsideClickTargetRefContext); if (!emojiProvider) { return null; } return jsx(EmojiPicker, { emojiProvider: Promise.resolve(emojiProvider), onSelection: props.updateEmoji, onPickerRef: setOutsideClickTargetRef }); }; // Note: These are based on the height and width of the emoji picker at the time // of writing (2025-05-05). It is 100% prone to change but at least it's vaguely // written down. var EMOJI_PICKER_MAX_HEIGHT = 431; var EMOJI_PICKER_MAX_WIDTH = 352; var EmojiPickerWithListener = withReactEditorViewOuterListeners(EmojiPickerWithProvider); export var EmojiPickerButton = function EmojiPickerButton(props) { var buttonRef = React.useRef(null); var _React$useState = React.useState(false), _React$useState2 = _slicedToArray(_React$useState, 2), isPopupOpen = _React$useState2[0], setIsPopupOpen = _React$useState2[1]; React.useEffect(function () { if (props.setDisableParentScroll) { props.setDisableParentScroll(isPopupOpen); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPopupOpen]); var togglePopup = useCallback(function () { setIsPopupOpen(!isPopupOpen); }, [setIsPopupOpen, isPopupOpen]); var updateEmoji = function updateEmoji(emoji) { setIsPopupOpen(false); props.onChange && props.onChange(emoji); requestAnimationFrame(function () { var _props$editorView; (_props$editorView = props.editorView) === null || _props$editorView === void 0 || _props$editorView.focus(); }); }; var isDetachedElement = useCallback(function (el) { return !document.body.contains(el); }, []); var handleEmojiClickOutside = useCallback(function (e) { // Ignore click events for detached elements. // Workaround for CETI-240 - where two onClicks fire - one when the upload button is // still in the document, and one once it's detached. Does not always occur, and // may be a side effect of a react render optimisation // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting if (e && e.target && !isDetachedElement(e.target)) { togglePopup(); } }, [isDetachedElement, togglePopup]); var handleEmojiPressEscape = useCallback(function () { var _buttonRef$current; setIsPopupOpen(false); (_buttonRef$current = buttonRef.current) === null || _buttonRef$current === void 0 || _buttonRef$current.focus(); }, [setIsPopupOpen, buttonRef]); var renderPopup = function renderPopup() { if (!buttonRef.current || !isPopupOpen) { return; } return jsx(Popup, { target: buttonRef.current // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion , mountTo: props.setDisableParentScroll ? props.mountPoint : buttonRef.current.parentElement, fitHeight: EMOJI_PICKER_MAX_HEIGHT, fitWidth: EMOJI_PICKER_MAX_WIDTH // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , offset: [0, 10] // Confluence inline comment editor has z-index: 500 // if the toolbar is scrollable, this will be mounted in the root editor // we need an index of > 500 to display over it , zIndex: props.setDisableParentScroll ? 600 : undefined, focusTrap: true, preventOverflow: true, boundariesElement: props.popupsBoundariesElement }, jsx(EmojiPickerWithListener, { handleEscapeKeydown: handleEmojiPressEscape, handleClickOutside: handleEmojiClickOutside, pluginInjectionApi: props.pluginInjectionApi, updateEmoji: updateEmoji })); }; var title = props.title || ''; return jsx("div", { css: emojiPickerButtonWrapperVisualRefresh }, jsx(Tooltip, { content: title, position: "top" }, jsx(IconButton, { appearance: "subtle", key: props.idx, onClick: togglePopup, ref: buttonRef, isSelected: props.isSelected, label: title, spacing: "compact" // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , icon: function icon() { return jsx(EmojiAddIcon, { color: "currentColor", label: "emoji-picker-button", spacing: "spacious" }); } })), renderPopup()); };