UNPKG

@atlaskit/editor-plugin-floating-toolbar

Version:

Floating toolbar plugin for @atlaskit/editor-core

139 lines (137 loc) 5.62 kB
/** * @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 const 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 } } }); const selector = states => { var _states$emojiState; return (_states$emojiState = states.emojiState) === null || _states$emojiState === void 0 ? void 0 : _states$emojiState.emojiProvider; }; const EmojiPickerWithProvider = props => { const emojiProvider = useSharedPluginStateWithSelector(props.pluginInjectionApi, ['emoji'], selector); const 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. const EMOJI_PICKER_MAX_HEIGHT = 431; const EMOJI_PICKER_MAX_WIDTH = 352; const EmojiPickerWithListener = withReactEditorViewOuterListeners(EmojiPickerWithProvider); export const EmojiPickerButton = props => { const buttonRef = React.useRef(null); const [isPopupOpen, setIsPopupOpen] = React.useState(false); React.useEffect(() => { if (props.setDisableParentScroll) { props.setDisableParentScroll(isPopupOpen); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPopupOpen]); const togglePopup = useCallback(() => { setIsPopupOpen(!isPopupOpen); }, [setIsPopupOpen, isPopupOpen]); const updateEmoji = emoji => { setIsPopupOpen(false); props.onChange && props.onChange(emoji); requestAnimationFrame(() => { var _props$editorView; (_props$editorView = props.editorView) === null || _props$editorView === void 0 ? void 0 : _props$editorView.focus(); }); }; const isDetachedElement = useCallback(el => !document.body.contains(el), []); const handleEmojiClickOutside = useCallback(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]); const handleEmojiPressEscape = useCallback(() => { var _buttonRef$current; setIsPopupOpen(false); (_buttonRef$current = buttonRef.current) === null || _buttonRef$current === void 0 ? void 0 : _buttonRef$current.focus(); }, [setIsPopupOpen, buttonRef]); const 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 })); }; const 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: () => jsx(EmojiAddIcon, { color: "currentColor", label: "emoji-picker-button", spacing: "spacious" }) })), renderPopup()); };