From 711b520421ff822c95fc750baa8a9d8667cd6c9e Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Tue, 8 Nov 2022 22:22:17 +0100 Subject: [PATCH] fix: Emoji picker overlay Signed-off-by: Tilman Vatteroth --- .../emoji-picker/emoji-picker-button.tsx | 36 ++++-- .../emoji-picker/emoji-picker-popover.tsx | 108 +++++++++++++++++ .../emoji-picker/emoji-picker.module.scss | 12 +- .../tool-bar/emoji-picker/emoji-picker.tsx | 109 ------------------ 4 files changed, 143 insertions(+), 122 deletions(-) create mode 100644 src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker-popover.tsx delete mode 100644 src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.tsx 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 ( -
- ) -}