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