Merge pull request #23940 from overleaf/td-react-18

Upgrade to React 18

GitOrigin-RevId: 9b81936e6eea2bccd97fe5c2c5841f0b946371b8
This commit is contained in:
Tim Down
2025-05-02 08:05:29 +00:00
committed by Copybot
parent 4464320757
commit 7abafb01ea
213 changed files with 2469 additions and 2105 deletions
@@ -2,7 +2,15 @@ import {
useCodeMirrorStateContext,
useCodeMirrorViewContext,
} from './codemirror-context'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
FC,
FormEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { runScopeHandlers } from '@codemirror/view'
import {
closeSearchPanel,
@@ -49,7 +57,7 @@ type MatchPositions = {
interrupted: boolean
}
const CodeMirrorSearchForm: FC = () => {
const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
const view = useCodeMirrorViewContext()
const state = useCodeMirrorStateContext()
const { setProjectSearchIsOpen } = useLayoutContext()
@@ -78,7 +86,7 @@ const CodeMirrorSearchForm: FC = () => {
const newEditor = useIsNewEditorEnabled()
const handleInputRef = useCallback(node => {
const handleInputRef = useCallback((node: HTMLInputElement) => {
inputRef.current = node
// focus the search input when the panel opens
@@ -88,11 +96,11 @@ const CodeMirrorSearchForm: FC = () => {
}
}, [])
const handleReplaceRef = useCallback(node => {
const handleReplaceRef = useCallback((node: HTMLInputElement) => {
replaceRef.current = node
}, [])
const handleSubmit = useCallback(event => {
const handleSubmit = useCallback((event: FormEvent) => {
event.preventDefault()
}, [])
@@ -125,8 +133,8 @@ const CodeMirrorSearchForm: FC = () => {
}, [handleChange, state, view])
const handleFormKeyDown = useCallback(
event => {
if (runScopeHandlers(view, event, 'search-panel')) {
(event: React.KeyboardEvent<HTMLFormElement>) => {
if (runScopeHandlers(view, event.nativeEvent, 'search-panel')) {
event.preventDefault()
}
},
@@ -135,7 +143,7 @@ const CodeMirrorSearchForm: FC = () => {
// Returns true if the event was handled, false otherwise
const handleEmacsNavigation = useCallback(
event => {
(event: KeyboardEvent) => {
const emacsCtrlSeq =
emacsKeybindingsActive &&
event.ctrlKey &&
@@ -163,7 +171,14 @@ const CodeMirrorSearchForm: FC = () => {
event.stopPropagation()
event.preventDefault()
closeSearchPanel(view)
document.dispatchEvent(new CustomEvent('cm:emacs-close-search-panel'))
// Wait for the search panel to close before moving the cursor
window.setTimeout(
() =>
document.dispatchEvent(
new CustomEvent('cm:emacs-close-search-panel')
),
0
)
return true
}
default: {
@@ -175,7 +190,7 @@ const CodeMirrorSearchForm: FC = () => {
)
const handleSearchKeyDown = useCallback(
event => {
(event: React.KeyboardEvent<HTMLInputElement>) => {
switch (event.key) {
case 'Enter':
event.preventDefault()
@@ -191,13 +206,13 @@ const CodeMirrorSearchForm: FC = () => {
}
break
}
handleEmacsNavigation(event)
handleEmacsNavigation(event.nativeEvent)
},
[view, handleEmacsNavigation, emacsKeybindingsActive]
)
const handleReplaceKeyDown = useCallback(
event => {
(event: React.KeyboardEvent<HTMLInputElement>) => {
switch (event.key) {
case 'Enter':
event.preventDefault()
@@ -216,7 +231,7 @@ const CodeMirrorSearchForm: FC = () => {
}
}
}
handleEmacsNavigation(event)
handleEmacsNavigation(event.nativeEvent)
},
[view, handleEmacsNavigation]
)
@@ -106,7 +106,7 @@ const Toolbar = memo(function Toolbar() {
// calculate overflow when buttons change
const observerRef = useRef<MutationObserver | null>(null)
const handleButtons = useCallback(
node => {
(node: HTMLDivElement) => {
if (!('MutationObserver' in window)) {
return
}
@@ -10,7 +10,7 @@ function CodeMirrorView() {
// append the editor view dom to the container node when mounted
const containerRef = useCallback(
node => {
(node: HTMLDivElement) => {
if (node) {
node.appendChild(view.dom)
}
@@ -1,4 +1,4 @@
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { FC, FormEvent, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
useCodeMirrorStateContext,
@@ -71,7 +71,7 @@ export const HrefTooltipContent: FC = () => {
}, [view])
const handleSubmit = useCallback(
event => {
(event: FormEvent) => {
event.preventDefault()
view.dispatch(closeCommandTooltip())
view.focus()
@@ -14,7 +14,7 @@ function EditorSwitch() {
const richTextAvailable = openDocName ? isValidTeXFile(openDocName) : false
const handleChange = useCallback(
event => {
(event: ChangeEvent<HTMLInputElement>) => {
const editorType = event.target.value
switch (editorType) {
@@ -80,7 +80,9 @@ const FigureModalExistingFigureContext = createContext<
| undefined
>(undefined)
export const FigureModalProvider: FC = ({ children }) => {
export const FigureModalProvider: FC<React.PropsWithChildren> = ({
children,
}) => {
const [state, dispatch] = useReducer(reducer, {
source: FigureModalSource.NONE,
helpShown: false,
@@ -1,7 +1,10 @@
import { FC } from 'react'
import { Trans, useTranslation } from 'react-i18next'
const LearnWikiLink: FC<{ article: string }> = ({ article, children }) => {
const LearnWikiLink: FC<React.PropsWithChildren<{ article: string }>> = ({
article,
children,
}) => {
return <a href={`/learn/latex/${article}`}>{children}</a>
}
@@ -104,7 +104,8 @@ const FigureModalContent = () => {
const hide = useCallback(() => {
dispatch({ source: FigureModalSource.NONE })
view.requestMeasure()
view.focus()
// Wait for the modal to close before focusing the editor
window.setTimeout(() => view.focus(), 0)
}, [dispatch, view])
useEventListener(
@@ -176,8 +176,8 @@ export const Cell: FC<{
}, [cellData.content, editing, view])
const onInput = useCallback(
e => {
update(filterInput(e.target.value))
(e: React.FormEvent<HTMLTextAreaElement>) => {
update(filterInput((e.target as HTMLTextAreaElement).value))
},
[update, filterInput]
)
@@ -38,7 +38,9 @@ export const useEditingContext = () => {
return context
}
export const EditingContextProvider: FC = ({ children }) => {
export const EditingContextProvider: FC<React.PropsWithChildren> = ({
children,
}) => {
const { table } = useTableContext()
const [cellData, setCellData] = useState<EditingContextData | null>(null)
const [initialContent, setInitialContent] = useState<string | undefined>(
@@ -85,7 +87,7 @@ export const EditingContextProvider: FC = ({ children }) => {
}, [setCellData])
const startEditing = useCallback(
(rowIndex: number, cellIndex: number, initialContent = undefined) => {
(rowIndex: number, cellIndex: number, initialContent?: string) => {
if (cellData?.dirty) {
// We're already editing something else
commitCellData()
@@ -411,7 +411,9 @@ export const useSelectionContext = () => {
return context
}
export const SelectionContextProvider: FC = ({ children }) => {
export const SelectionContextProvider: FC<React.PropsWithChildren> = ({
children,
}) => {
const [selection, setSelection] = useState<TableSelection | null>(null)
const [dragging, setDragging] = useState(false)
return (
@@ -34,13 +34,15 @@ const TableContext = createContext<
| undefined
>(undefined)
export const TableProvider: FC<{
tableData: ParsedTableData
tableNode: SyntaxNode | null
tabularNode: SyntaxNode
view: EditorView
directTableChild?: boolean
}> = ({
export const TableProvider: FC<
React.PropsWithChildren<{
tableData: ParsedTableData
tableNode: SyntaxNode | null
tabularNode: SyntaxNode
view: EditorView
directTableChild?: boolean
}>
> = ({
tableData,
children,
tableNode,
@@ -21,7 +21,7 @@ const TabularContext = createContext<
| undefined
>(undefined)
export const TabularProvider: FC = ({ children }) => {
export const TabularProvider: FC<React.PropsWithChildren> = ({ children }) => {
const ref = useRef<HTMLDivElement>(null)
const [helpShown, setHelpShown] = useState(false)
const [columnWidthModalShown, setColumnWidthModalShown] = useState(false)
@@ -273,7 +273,7 @@ export const Tabular: FC<{
)
}
const TabularWrapper: FC = () => {
const TabularWrapper: FC<React.PropsWithChildren> = () => {
const { setSelection, selection } = useSelectionContext()
const { commitCellData, cellData } = useEditingContext()
const { ref } = useTabularContext()
@@ -7,13 +7,15 @@ import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import MaterialIcon from '../../../../../shared/components/material-icon'
import { useTabularContext } from '../contexts/tabular-context'
export const ToolbarButtonMenu: FC<{
id: string
label: string
icon: string
disabled?: boolean
disabledLabel?: string
}> = memo(function ButtonMenu({
export const ToolbarButtonMenu: FC<
React.PropsWithChildren<{
id: string
label: string
icon: string
disabled?: boolean
disabledLabel?: string
}>
> = memo(function ButtonMenu({
icon,
id,
label,
@@ -28,12 +28,12 @@ export const ToolbarButton = memo<{
disabledLabel,
}) {
const view = useCodeMirrorViewContext()
const handleMouseDown = useCallback(event => {
const handleMouseDown = useCallback((event: React.MouseEvent) => {
event.preventDefault()
}, [])
const handleClick = useCallback(
event => {
(event: React.MouseEvent) => {
if (command) {
emitTableGeneratorEvent(view, id)
event.preventDefault()
@@ -9,16 +9,18 @@ import { emitTableGeneratorEvent } from '../analytics'
import { useCodeMirrorViewContext } from '../../codemirror-context'
import classNames from 'classnames'
export const ToolbarDropdown: FC<{
id: string
label?: string
btnClassName?: string
icon?: string
tooltip?: string
disabled?: boolean
disabledTooltip?: string
showCaret?: boolean
}> = ({
export const ToolbarDropdown: FC<
React.PropsWithChildren<{
id: string
label?: string
btnClassName?: string
icon?: string
tooltip?: string
disabled?: boolean
disabledTooltip?: string
showCaret?: boolean
}>
> = ({
id,
label,
children,
@@ -112,12 +114,14 @@ export const ToolbarDropdown: FC<{
}
export const ToolbarDropdownItem: FC<
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> & {
command: () => void
id: string
icon?: string
active?: boolean
}
React.PropsWithChildren<
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> & {
command: () => void
id: string
icon?: string
active?: boolean
}
>
> = ({ children, command, id, icon, active, ...props }) => {
const view = useCodeMirrorViewContext()
const onClick = useCallback(() => {
@@ -8,12 +8,14 @@ import { EditorView } from '@codemirror/view'
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
import { useCodeMirrorViewContext } from '../codemirror-context'
export const ToolbarButtonMenu: FC<{
id: string
label: string
icon: React.ReactNode
altCommand?: (view: EditorView) => void
}> = memo(function ButtonMenu({ icon, id, label, altCommand, children }) {
export const ToolbarButtonMenu: FC<
React.PropsWithChildren<{
id: string
label: string
icon: React.ReactNode
altCommand?: (view: EditorView) => void
}>
> = memo(function ButtonMenu({ icon, id, label, altCommand, children }) {
const target = useRef<any>(null)
const { open, onToggle, ref } = useDropdown()
const view = useCodeMirrorViewContext()
@@ -6,14 +6,15 @@ import { useCodeMirrorViewContext } from '../codemirror-context'
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
import OLPopover from '@/features/ui/components/ol/ol-popover'
export const ToolbarOverflow: FC<{
overflowed: boolean
overflowOpen: boolean
setOverflowOpen: (open: boolean) => void
overflowRef?: React.Ref<HTMLDivElement>
}> = ({ overflowed, overflowOpen, setOverflowOpen, overflowRef, children }) => {
export const ToolbarOverflow: FC<
React.PropsWithChildren<{
overflowed: boolean
overflowOpen: boolean
setOverflowOpen: (open: boolean) => void
overflowRef?: React.Ref<HTMLDivElement>
}>
> = ({ overflowed, overflowOpen, setOverflowOpen, overflowRef, children }) => {
const { t } = useTranslation()
const buttonRef = useRef<HTMLButtonElement>(null)
const keyboardInputRef = useRef(false)
const view = useCodeMirrorViewContext()
@@ -31,12 +31,12 @@ export const ToolbarButton = memo<{
}) {
const view = useCodeMirrorViewContext()
const handleMouseDown = useCallback(event => {
const handleMouseDown = useCallback((event: React.MouseEvent) => {
event.preventDefault()
}, [])
const handleClick = useCallback(
event => {
(event: React.MouseEvent) => {
emitToolbarEvent(view, id)
if (command) {
event.preventDefault()
@@ -1,3 +1,4 @@
import { createRoot } from 'react-dom/client'
import {
StateField,
StateEffect,
@@ -12,7 +13,6 @@ import {
getSpellCheckLanguage,
} from '@/features/source-editor/extensions/spelling/index'
import { sendMB } from '@/infrastructure/event-tracking'
import ReactDOM from 'react-dom'
import { SpellingSuggestions } from '@/features/source-editor/extensions/spelling/spelling-suggestions'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { addLearnedWord } from '@/features/source-editor/extensions/spelling/learned-words'
@@ -159,7 +159,8 @@ const createSpellingSuggestionList = (word: Word) => (view: EditorView) => {
const dom = document.createElement('div')
dom.classList.add('ol-cm-spelling-context-menu-tooltip')
ReactDOM.render(
const root = createRoot(dom)
root.render(
<SplitTestProvider>
<SpellingSuggestions
word={word}
@@ -235,12 +236,11 @@ const createSpellingSuggestionList = (word: Word) => (view: EditorView) => {
})
}}
/>
</SplitTestProvider>,
dom
</SplitTestProvider>
)
const destroy = () => {
ReactDOM.unmountComponentAtNode(dom)
root.unmount()
}
return { dom, destroy }
@@ -7,7 +7,7 @@ import {
import { Decoration, EditorView, WidgetType } from '@codemirror/view'
import { undo } from '@codemirror/commands'
import { ancestorNodeOfType } from '../../utils/tree-operations/ancestors'
import ReactDOM from 'react-dom'
import { createRoot } from 'react-dom/client'
import { PastedContentMenu } from '../../components/paste-html/pasted-content-menu'
import { SplitTestProvider } from '../../../../shared/context/split-test-context'
@@ -171,7 +171,8 @@ class PastedContentMenuWidget extends WidgetType {
toDOM(view: EditorView) {
const element = document.createElement('span')
ReactDOM.render(
const root = createRoot(element)
root.render(
<SplitTestProvider>
<PastedContentMenu
insertPastedContent={this.insertPastedContent}
@@ -179,8 +180,7 @@ class PastedContentMenuWidget extends WidgetType {
formatted={this.formatted}
pastedContent={this.pastedContent}
/>
</SplitTestProvider>,
element
</SplitTestProvider>
)
return element
}
@@ -1,10 +1,12 @@
import { createRoot, Root } from 'react-dom/client'
import { EditorView, WidgetType } from '@codemirror/view'
import { SyntaxNode } from '@lezer/common'
import * as ReactDOM from 'react-dom'
import { Tabular } from '../../../components/table-generator/tabular'
import { ParsedTableData } from '../../../components/table-generator/utils'
export class TabularWidget extends WidgetType {
static roots: WeakMap<HTMLElement, Root> = new WeakMap()
constructor(
private parsedTableData: ParsedTableData,
private tabularNode: SyntaxNode,
@@ -15,13 +17,21 @@ export class TabularWidget extends WidgetType {
super()
}
renderInDOMContainer(children: React.ReactNode, element: HTMLElement) {
const root = TabularWidget.roots.get(element) || createRoot(element)
if (!TabularWidget.roots.has(element)) {
TabularWidget.roots.set(element, root)
}
root.render(children)
}
toDOM(view: EditorView) {
const element = document.createElement('div')
element.classList.add('ol-cm-tabular')
if (this.tableNode) {
element.classList.add('ol-cm-environment-table')
}
ReactDOM.render(
this.renderInDOMContainer(
<Tabular
view={view}
tabularNode={this.tabularNode}
@@ -46,7 +56,7 @@ export class TabularWidget extends WidgetType {
}
updateDOM(element: HTMLElement, view: EditorView): boolean {
ReactDOM.render(
this.renderInDOMContainer(
<Tabular
view={view}
tabularNode={this.tabularNode}
@@ -68,6 +78,10 @@ export class TabularWidget extends WidgetType {
}
destroy(element: HTMLElement) {
ReactDOM.unmountComponentAtNode(element)
const root = TabularWidget.roots.get(element)
if (root) {
TabularWidget.roots.delete(element)
root.unmount()
}
}
}
@@ -51,7 +51,11 @@ import { updateRanges } from '@/features/source-editor/extensions/ranges'
import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
import { Permissions } from '@/features/ide-react/types/permissions'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import {
GotoOffsetOptions,
useEditorManagerContext,
} from '@/features/ide-react/context/editor-manager-context'
import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options'
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
function useCodeMirrorScope(view: EditorView) {
@@ -441,7 +445,7 @@ function useCodeMirrorScope(view: EditorView) {
useScopeEventListener(
'editor:gotoLine',
useCallback(
(_event, options) => {
(_event: any, options: GotoLineOptions) => {
setCursorLineAndScroll(
view,
options.gotoLine,
@@ -460,7 +464,7 @@ function useCodeMirrorScope(view: EditorView) {
useScopeEventListener(
'editor:gotoOffset',
useCallback(
(_event, options) => {
(_event: any, options: GotoOffsetOptions) => {
setCursorPositionAndScroll(view, options.gotoOffset)
},
[view]