@atlaskit/editor-plugin-floating-toolbar
Version:
Floating toolbar plugin for @atlaskit/editor-core
147 lines (145 loc) • 5.97 kB
JavaScript
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());
};