Merge pull request #24688 from overleaf/td-bs5-editor-beginner-switch-popover

Migrate beginner editor switch popover to BS5

GitOrigin-RevId: c470df46989de7ad6477ee23ff13fc95dd580ea8
This commit is contained in:
Tim Down
2025-04-07 14:56:26 +01:00
committed by Copybot
parent b3b0f07ae9
commit eb77146c0c
3 changed files with 114 additions and 124 deletions

View File

@@ -0,0 +1,102 @@
import { ReactElement, useCallback, useEffect, useState } from 'react'
import Close from '@/shared/components/close'
import { useEditorContext } from '@/shared/context/editor-context'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useUserContext } from '@/shared/context/user-context'
import useScopeValue from '@/shared/hooks/use-scope-value'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
import OLPopover from '@/features/ui/components/ol/ol-popover'
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
const CODE_EDITOR_POPOVER_TIMEOUT = 1000
export const codeEditorModePrompt = 'code-editor-mode-prompt'
export const EditorSwitchBeginnerPopover = ({
children,
targetRef,
}: {
children: ReactElement
targetRef: React.RefObject<HTMLElement>
}) => {
const user = useUserContext()
const { inactiveTutorials } = useEditorContext()
const { t } = useTranslation()
const [codeEditorOpened] = useScopeValue('editor.codeEditorOpened')
const { completeTutorial } = useTutorial(codeEditorModePrompt, {
location: 'logs',
name: codeEditorModePrompt,
})
const [popoverShown, setPopoverShown] = useState(false)
const shouldShowCodeEditorPopover = useCallback(() => {
if (inactiveTutorials.includes(codeEditorModePrompt)) {
return false
}
if (getMeta('ol-usedLatex') !== 'never') {
// only show popover to the users that never used LaTeX (submitted in onboarding data collection)
return false
}
if (codeEditorOpened) {
// dont show popover if code editor was opened at some point
return false
}
const msSinceSignedUp =
user.signUpDate && Date.now() - new Date(user.signUpDate).getTime()
if (msSinceSignedUp && msSinceSignedUp < 24 * 60 * 60 * 1000) {
// dont show popover if user has signed up is less than 24 hours
return false
}
return true
}, [codeEditorOpened, inactiveTutorials, user.signUpDate])
useEffect(() => {
if (popoverShown && codeEditorOpened) {
setPopoverShown(false)
}
}, [codeEditorOpened, popoverShown])
useEffect(() => {
const timeout = setTimeout(() => {
if (shouldShowCodeEditorPopover()) {
setPopoverShown(true)
}
}, CODE_EDITOR_POPOVER_TIMEOUT)
return () => clearTimeout(timeout)
}, [shouldShowCodeEditorPopover])
return (
<>
{children}
<OLOverlay
placement="bottom"
show={popoverShown}
rootClose
onHide={() => setPopoverShown(false)}
target={targetRef.current}
>
<OLPopover id="editor-switch-popover">
<div>
<Close
variant="dark"
onDismiss={() => {
setPopoverShown(false)
completeTutorial({ event: 'promo-click', action: 'complete' })
}}
/>
<div className="tooltip-title">
{t('code_editor_tooltip_title')}
</div>
<div>{t('code_editor_tooltip_message')}</div>
</div>
</OLPopover>
</OLOverlay>
</>
)
}

View File

@@ -1,118 +0,0 @@
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react'
import Tooltip from '../../../shared/components/tooltip'
import Close from '@/shared/components/close'
import { useEditorContext } from '@/shared/context/editor-context'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useUserContext } from '@/shared/context/user-context'
import useScopeValue from '@/shared/hooks/use-scope-value'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
const CODE_EDITOR_TOOLTIP_TIMEOUT = 1000
export const codeEditorModePrompt = 'code-editor-mode-prompt'
export const EditorSwitchBeginnerTooltip = ({
children,
}: {
children: ReactElement
}) => {
const toolbarRef = useRef<any>(null)
const user = useUserContext()
const { inactiveTutorials } = useEditorContext()
const { t } = useTranslation()
const [codeEditorOpened] = useScopeValue('editor.codeEditorOpened')
const { completeTutorial } = useTutorial(codeEditorModePrompt, {
location: 'logs',
name: codeEditorModePrompt,
})
const [tooltipShown, setTooltipShown] = useState(false)
const shouldShowCodeEditorTooltip = useCallback(() => {
if (inactiveTutorials.includes(codeEditorModePrompt)) {
return false
}
if (getMeta('ol-usedLatex') !== 'never') {
// only show tooltip to the users that never used LaTeX (submitted in onboarding data collection)
return false
}
if (codeEditorOpened) {
// dont show tooltip if code editor was opened at some point
return false
}
const msSinceSignedUp =
user.signUpDate && Date.now() - new Date(user.signUpDate).getTime()
if (msSinceSignedUp && msSinceSignedUp < 24 * 60 * 60 * 1000) {
// dont show tooltip if user has signed up is less than 24 hours
return false
}
return true
}, [codeEditorOpened, inactiveTutorials, user.signUpDate])
const showCodeEditorTooltip = useCallback(() => {
if (toolbarRef.current && 'show' in toolbarRef.current) {
toolbarRef.current.show()
setTooltipShown(true)
}
}, [])
const hideCodeEditorTooltip = useCallback(() => {
if (toolbarRef.current && 'hide' in toolbarRef.current) {
toolbarRef.current.hide()
setTooltipShown(false)
}
}, [])
useEffect(() => {
if (tooltipShown && codeEditorOpened) {
hideCodeEditorTooltip()
}
}, [codeEditorOpened, hideCodeEditorTooltip, tooltipShown])
useEffect(() => {
const timeout = setTimeout(() => {
if (shouldShowCodeEditorTooltip()) {
showCodeEditorTooltip()
}
}, CODE_EDITOR_TOOLTIP_TIMEOUT)
return () => clearTimeout(timeout)
}, [showCodeEditorTooltip, shouldShowCodeEditorTooltip])
return (
<Tooltip
id="editor-switch-tooltip"
description={
<div>
<Close
variant="dark"
onDismiss={() => {
hideCodeEditorTooltip()
completeTutorial({ event: 'promo-click', action: 'complete' })
}}
/>
<div className="tooltip-title">{t('code_editor_tooltip_title')}</div>
<div>{t('code_editor_tooltip_message')}</div>
</div>
}
tooltipProps={{
className: 'editor-switch-tooltip',
}}
overlayProps={{
ref: toolbarRef,
placement: 'bottom',
shouldUpdatePosition: true,
// @ts-ignore
// trigger: null is used to prevent the tooltip from showing on hover
// but it is not allowed in the type definition
trigger: null,
}}
>
{children}
</Tooltip>
)
}

View File

@@ -1,4 +1,4 @@
import { ChangeEvent, FC, memo, useCallback } from 'react'
import { ChangeEvent, FC, memo, useCallback, useRef } from 'react'
import useScopeValue from '@/shared/hooks/use-scope-value'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
@@ -6,9 +6,9 @@ import { sendMB } from '../../../infrastructure/event-tracking'
import { isValidTeXFile } from '../../../main/is-valid-tex-file'
import { useTranslation } from 'react-i18next'
import {
EditorSwitchBeginnerTooltip,
EditorSwitchBeginnerPopover,
codeEditorModePrompt,
} from './editor-switch-beginner-tooltip'
} from './editor-switch-beginner-popover'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
function EditorSwitch() {
@@ -17,6 +17,8 @@ function EditorSwitch() {
const [codeEditorOpened] = useScopeValue('editor.codeEditorOpened')
const { openDocName } = useEditorManagerContext()
const codeEditorRef = useRef<HTMLLabelElement>(null)
const richTextAvailable = openDocName ? isValidTeXFile(openDocName) : false
const { completeTutorial } = useTutorial(codeEditorModePrompt, {
location: 'logs',
@@ -62,11 +64,15 @@ function EditorSwitch() {
checked={!richTextAvailable || !visual}
onChange={handleChange}
/>
<EditorSwitchBeginnerTooltip>
<label htmlFor="editor-switch-cm6" className="toggle-switch-label">
<EditorSwitchBeginnerPopover targetRef={codeEditorRef}>
<label
ref={codeEditorRef}
htmlFor="editor-switch-cm6"
className="toggle-switch-label"
>
<span>{t('code_editor')}</span>
</label>
</EditorSwitchBeginnerTooltip>
</EditorSwitchBeginnerPopover>
<RichTextToggle
checked={richTextAvailable && visual}