diff --git a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker-button.tsx b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker-button.tsx
index 7a961702d..32ab4e20a 100644
--- a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker-button.tsx
+++ b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker-button.tsx
@@ -4,48 +4,66 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import React, { Fragment, useCallback, useState } from 'react'
-import { Button } from 'react-bootstrap'
+import React, { Fragment, useCallback, useRef, useState } from 'react'
+import { Button, Overlay } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
-import { EmojiPicker } from './emoji-picker'
+import { EmojiPickerPopover } from './emoji-picker-popover'
import { cypressId } from '../../../../../utils/cypress-attribute'
import type { EmojiClickEventDetail } from 'emoji-picker-element/shared'
-import { Optional } from '@mrdrogdrog/optional'
import { useChangeEditorContentCallback } from '../../../change-content-context/use-change-editor-content-callback'
import { replaceSelection } from '../formatters/replace-selection'
import { extractEmojiShortCode } from './extract-emoji-short-code'
+import styles from './emoji-picker.module.scss'
+import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
/**
* Renders a button to open the emoji picker.
- * @see EmojiPicker
+ * @see EmojiPickerPopover
*/
export const EmojiPickerButton: React.FC = () => {
const { t } = useTranslation()
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const changeEditorContent = useChangeEditorContentCallback()
+ const buttonRef = useRef(null)
const onEmojiSelected = useCallback(
(emojiClickEvent: EmojiClickEventDetail) => {
setShowEmojiPicker(false)
- Optional.ofNullable(extractEmojiShortCode(emojiClickEvent)).ifPresent((shortCode) => {
+ const shortCode = extractEmojiShortCode(emojiClickEvent)
+ if (shortCode) {
changeEditorContent?.(({ currentSelection }) => replaceSelection(currentSelection, shortCode, false))
- })
+ }
},
[changeEditorContent]
)
const hidePicker = useCallback(() => setShowEmojiPicker(false), [])
const showPicker = useCallback(() => setShowEmojiPicker(true), [])
+ const createPopoverElement = useCallback<(props: OverlayInjectedProps) => React.ReactElement>(
+ (props) => ,
+ [onEmojiSelected]
+ )
+
return (
-
+
+ {createPopoverElement}
+
diff --git a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker-popover.tsx b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker-popover.tsx
new file mode 100644
index 000000000..0ccf7e1c0
--- /dev/null
+++ b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker-popover.tsx
@@ -0,0 +1,108 @@
+/*
+ * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Picker } from 'emoji-picker-element'
+import type { CustomEmoji, EmojiClickEvent, EmojiClickEventDetail } from 'emoji-picker-element/shared'
+import React, { useEffect, useRef } from 'react'
+import { useIsDarkModeActivated } from '../../../../../hooks/common/use-is-dark-mode-activated'
+import styles from './emoji-picker.module.scss'
+import forkawesomeIcon from './forkawesome.png'
+import { ForkAwesomeIcons } from '../../../../common/fork-awesome/fork-awesome-icons'
+import fontStyles from '../../../../../../global-styles/variables.module.scss'
+import { Popover } from 'react-bootstrap'
+import type { PopoverProps } from 'react-bootstrap/Popover'
+
+const customEmojis: CustomEmoji[] = ForkAwesomeIcons.map((name) => ({
+ name: `fa-${name}`,
+ shortcodes: [`fa-${name.toLowerCase()}`],
+ url: forkawesomeIcon.src,
+ category: 'ForkAwesome'
+}))
+
+const EMOJI_DATA_PATH = '_next/static/js/emoji-data.json'
+
+const emojiPickerConfig = {
+ customEmoji: customEmojis,
+ dataSource: EMOJI_DATA_PATH
+}
+
+const twemojiStyle = (): HTMLStyleElement => {
+ const style = document.createElement('style')
+ style.textContent = `section.picker { --font-family: ${fontStyles['font-family-emojis']} !important; }`
+ return style
+}
+
+export interface EmojiPickerProps extends PopoverProps {
+ onEmojiSelected: (emoji: EmojiClickEventDetail) => void
+}
+
+/**
+ * Renders the emoji picker.
+ *
+ * @param show If the emoji picker should be shown
+ * @param onEmojiSelected The callback, that will be called if an emoji is selected
+ * @param onDismiss The callback, that will be called if the picker should be closed.
+ * @external {Picker} https://www.npmjs.com/package/emoji-picker-element
+ */
+export const EmojiPickerPopover = React.forwardRef(
+ ({ onEmojiSelected, ...props }, ref) => {
+ const darkModeEnabled = useIsDarkModeActivated()
+ const pickerContainerRef = useRef(null)
+ const pickerRef = useRef()
+
+ useEffect(() => {
+ if (!pickerContainerRef.current) {
+ return
+ }
+ const picker = new Picker(emojiPickerConfig)
+ if (picker.shadowRoot) {
+ picker.shadowRoot.appendChild(twemojiStyle())
+ }
+ pickerContainerRef.current.appendChild(picker)
+
+ pickerRef.current = picker
+ return () => {
+ picker.remove()
+ pickerRef.current = undefined
+ }
+ }, [])
+
+ useEffect(() => {
+ if (!pickerRef.current) {
+ return
+ }
+ const emojiClick = (event: EmojiClickEvent): void => {
+ onEmojiSelected(event.detail)
+ }
+ const picker = pickerRef.current
+ picker.addEventListener('emoji-click', emojiClick, true)
+ return () => {
+ picker.removeEventListener('emoji-click', emojiClick, true)
+ }
+ }, [onEmojiSelected])
+
+ useEffect(() => {
+ if (!pickerRef.current) {
+ return
+ }
+ pickerRef.current.setAttribute('class', darkModeEnabled ? 'dark' : 'light')
+ if (darkModeEnabled) {
+ pickerRef.current.removeAttribute('style')
+ } else {
+ pickerRef.current.setAttribute('style', '--background: #f8f9fa')
+ }
+ }, [darkModeEnabled])
+
+ return (
+
+
+
+
+
+ )
+ }
+)
+EmojiPickerPopover.displayName = 'EmojiPickerPopover'
diff --git a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.module.scss b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.module.scss
index 12794da5f..a184d81d7 100644
--- a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.module.scss
+++ b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.module.scss
@@ -1,9 +1,13 @@
-/*
- * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
+/*!
+ * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
-.emoji-picker-container {
- z-index: 1111;
+.tooltip {
+ &, :global(body.dark) & {
+ --bs-popover-max-width: 100%;
+ --bs-popover-body-padding-y: 0;
+ --bs-popover-body-padding-x: 0;
+ }
}
diff --git a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.tsx b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.tsx
deleted file mode 100644
index 7ee28b10e..000000000
--- a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Picker } from 'emoji-picker-element'
-import type { CustomEmoji, EmojiClickEvent, EmojiClickEventDetail } from 'emoji-picker-element/shared'
-import React, { useEffect, useRef } from 'react'
-import { useClickAway } from 'react-use'
-import { useIsDarkModeActivated } from '../../../../../hooks/common/use-is-dark-mode-activated'
-import styles from './emoji-picker.module.scss'
-import forkawesomeIcon from './forkawesome.png'
-import { ForkAwesomeIcons } from '../../../../common/fork-awesome/fork-awesome-icons'
-import fontStyles from '../../../../../../global-styles/variables.module.scss'
-
-const customEmojis: CustomEmoji[] = ForkAwesomeIcons.map((name) => ({
- name: `fa-${name}`,
- shortcodes: [`fa-${name.toLowerCase()}`],
- url: forkawesomeIcon.src,
- category: 'ForkAwesome'
-}))
-
-const EMOJI_DATA_PATH = '_next/static/js/emoji-data.json'
-
-const emojiPickerConfig = {
- customEmoji: customEmojis,
- dataSource: EMOJI_DATA_PATH
-}
-
-const twemojiStyle = (): HTMLStyleElement => {
- const style = document.createElement('style')
- style.textContent = `section.picker { --font-family: ${fontStyles['font-family-emojis']} !important; }`
- return style
-}
-
-export interface EmojiPickerProps {
- show: boolean
- onEmojiSelected: (emoji: EmojiClickEventDetail) => void
- onDismiss: () => void
-}
-
-/**
- * Renders the emoji picker.
- *
- * @param show If the emoji picker should be shown
- * @param onEmojiSelected The callback, that will be called if an emoji is selected
- * @param onDismiss The callback, that will be called if the picker should be closed.
- * @external {Picker} https://www.npmjs.com/package/emoji-picker-element
- */
-export const EmojiPicker: React.FC = ({ show, onEmojiSelected, onDismiss }) => {
- const darkModeEnabled = useIsDarkModeActivated()
- const pickerContainerRef = useRef(null)
- const pickerRef = useRef()
-
- useClickAway(pickerContainerRef, () => {
- onDismiss()
- })
-
- useEffect(() => {
- if (!pickerContainerRef.current) {
- return
- }
- const picker = new Picker(emojiPickerConfig)
- if (picker.shadowRoot) {
- picker.shadowRoot.appendChild(twemojiStyle())
- }
- pickerContainerRef.current.appendChild(picker)
-
- pickerRef.current = picker
- return () => {
- picker.remove()
- pickerRef.current = undefined
- }
- }, [])
-
- useEffect(() => {
- if (!pickerRef.current) {
- return
- }
- const emojiClick = (event: EmojiClickEvent): void => {
- onEmojiSelected(event.detail)
- }
- const picker = pickerRef.current
- picker.addEventListener('emoji-click', emojiClick, true)
- return () => {
- picker.removeEventListener('emoji-click', emojiClick, true)
- }
- }, [onEmojiSelected])
-
- useEffect(() => {
- if (!pickerRef.current) {
- return
- }
- pickerRef.current.setAttribute('class', darkModeEnabled ? 'dark' : 'light')
- if (darkModeEnabled) {
- pickerRef.current.removeAttribute('style')
- } else {
- pickerRef.current.setAttribute('style', '--background: #f8f9fa')
- }
- }, [darkModeEnabled])
-
- return (
-
- )
-}