[cm6] Improve performance of the editor toolbar (#13915)

* Memoize figure and math dropdowns
* Only build section heading overlay when open
* Memoise currentLevel
* Remove memo from ToolbarOverflow
* Calculate listDepth in the container component
* Avoid using document.querySelector

GitOrigin-RevId: d5ec8817d35d04e0e2c60c8eecc8678ede69f82a
This commit is contained in:
Alf Eaton 2023-07-18 11:23:51 +01:00 committed by Copybot
parent 930cec2189
commit ff7eec48de
7 changed files with 86 additions and 64 deletions

View file

@ -21,6 +21,7 @@ import getMeta from '../../../utils/meta'
import { isVisual } from '../extensions/visual/visual' import { isVisual } from '../extensions/visual/visual'
import SplitTestBadge from '../../../shared/components/split-test-badge' import SplitTestBadge from '../../../shared/components/split-test-badge'
import { language } from '@codemirror/language' import { language } from '@codemirror/language'
import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors'
export const CodeMirrorToolbar = () => { export const CodeMirrorToolbar = () => {
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
@ -48,6 +49,8 @@ const Toolbar = memo(function Toolbar() {
const languageName = state.facet(language)?.name const languageName = state.facet(language)?.name
const visual = isVisual(view) const visual = isVisual(view)
const listDepth = minimumListDepthForSelection(state)
const { const {
open: overflowOpen, open: overflowOpen,
onToggle: setOverflowOpen, onToggle: setOverflowOpen,
@ -108,7 +111,12 @@ const Toolbar = memo(function Toolbar() {
return ( return (
<div className="ol-cm-toolbar toolbar-editor" ref={elementRef}> <div className="ol-cm-toolbar toolbar-editor" ref={elementRef}>
{showSourceToolbar && <EditorSwitch />} {showSourceToolbar && <EditorSwitch />}
<ToolbarItems state={state} languageName={languageName} visual={visual} /> <ToolbarItems
state={state}
languageName={languageName}
visual={visual}
listDepth={listDepth}
/>
<div <div
className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch" className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch"
ref={overflowBeforeRef} ref={overflowBeforeRef}
@ -125,6 +133,7 @@ const Toolbar = memo(function Toolbar() {
overflowed={overflowedItemsRef.current} overflowed={overflowedItemsRef.current}
languageName={languageName} languageName={languageName}
visual={visual} visual={visual}
listDepth={listDepth}
/> />
</ToolbarOverflow> </ToolbarOverflow>
<div className="formatting-buttons-wrapper" /> <div className="formatting-buttons-wrapper" />

View file

@ -57,7 +57,7 @@ export const ToolbarButtonMenu: FC<{
show={open} show={open}
target={target.current} target={target.current}
placement="bottom" placement="bottom"
container={document.querySelector('.cm-editor')} container={view.dom}
containerPadding={0} containerPadding={0}
animation animation
onHide={() => onToggle(false)} onHide={() => onToggle(false)}

View file

@ -1,14 +1,14 @@
import { ListGroupItem } from 'react-bootstrap' import { ListGroupItem } from 'react-bootstrap'
import { ToolbarButtonMenu } from './button-menu' import { ToolbarButtonMenu } from './button-menu'
import Icon from '../../../../shared/components/icon' import Icon from '../../../../shared/components/icon'
import { useCallback } from 'react' import { memo, useCallback } from 'react'
import { FigureModalSource } from '../figure-modal/figure-modal-context' import { FigureModalSource } from '../figure-modal/figure-modal-context'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
import { useCodeMirrorViewContext } from '../codemirror-editor' import { useCodeMirrorViewContext } from '../codemirror-editor'
import { insertFigure } from '../../extensions/toolbar/commands' import { insertFigure } from '../../extensions/toolbar/commands'
export const InsertFigureDropdown = () => { export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
const { t } = useTranslation() const { t } = useTranslation()
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
const openFigureModal = useCallback( const openFigureModal = useCallback(
@ -57,4 +57,4 @@ export const InsertFigureDropdown = () => {
</ListGroupItem> </ListGroupItem>
</ToolbarButtonMenu> </ToolbarButtonMenu>
) )
} })

View file

@ -8,8 +8,9 @@ import {
wrapInDisplayMath, wrapInDisplayMath,
wrapInInlineMath, wrapInInlineMath,
} from '../../extensions/toolbar/commands' } from '../../extensions/toolbar/commands'
import { memo } from 'react'
export function MathDropdown() { export const MathDropdown = memo(function MathDropdown() {
const { t } = useTranslation() const { t } = useTranslation()
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
@ -46,4 +47,4 @@ export function MathDropdown() {
</ListGroupItem> </ListGroupItem>
</ToolbarButtonMenu> </ToolbarButtonMenu>
) )
} })

View file

@ -1,7 +1,8 @@
import { FC, LegacyRef, memo } from 'react' import { FC, LegacyRef } from 'react'
import { Button, Overlay, Popover } from 'react-bootstrap' import { Button, Overlay, Popover } from 'react-bootstrap'
import classnames from 'classnames' import classnames from 'classnames'
import Icon from '../../../../shared/components/icon' import Icon from '../../../../shared/components/icon'
import { useCodeMirrorViewContext } from '../codemirror-editor'
export const ToolbarOverflow: FC<{ export const ToolbarOverflow: FC<{
overflowed: boolean overflowed: boolean
@ -9,14 +10,16 @@ export const ToolbarOverflow: FC<{
overflowOpen: boolean overflowOpen: boolean
setOverflowOpen: (open: boolean) => void setOverflowOpen: (open: boolean) => void
overflowRef?: LegacyRef<Popover> overflowRef?: LegacyRef<Popover>
}> = memo(function ToolbarOverflow({ }> = ({
overflowed, overflowed,
target, target,
overflowOpen, overflowOpen,
setOverflowOpen, setOverflowOpen,
overflowRef, overflowRef,
children, children,
}) { }) => {
const view = useCodeMirrorViewContext()
const className = classnames( const className = classnames(
'ol-cm-toolbar-button', 'ol-cm-toolbar-button',
'ol-cm-toolbar-overflow-toggle', 'ol-cm-toolbar-overflow-toggle',
@ -48,7 +51,7 @@ export const ToolbarOverflow: FC<{
show={overflowOpen} show={overflowOpen}
target={target} target={target}
placement="bottom" placement="bottom"
container={document.querySelector('.cm-editor')} container={view.dom}
containerPadding={0} containerPadding={0}
animation animation
onHide={() => setOverflowOpen(false)} onHide={() => setOverflowOpen(false)}
@ -59,4 +62,4 @@ export const ToolbarOverflow: FC<{
</Overlay> </Overlay>
</> </>
) )
}) }

View file

@ -7,7 +7,7 @@ import {
findCurrentSectionHeadingLevel, findCurrentSectionHeadingLevel,
setSectionHeadingLevel, setSectionHeadingLevel,
} from '../../extensions/toolbar/sections' } from '../../extensions/toolbar/sections'
import { useCallback, useRef } from 'react' import { useCallback, useMemo, useRef } from 'react'
import { Overlay, Popover } from 'react-bootstrap' import { Overlay, Popover } from 'react-bootstrap'
import useEventListener from '../../../../shared/hooks/use-event-listener' import useEventListener from '../../../../shared/hooks/use-event-listener'
import useDropdown from '../../../../shared/hooks/use-dropdown' import useDropdown from '../../../../shared/hooks/use-dropdown'
@ -42,7 +42,11 @@ export const SectionHeadingDropdown = () => {
const toggleButtonRef = useRef<HTMLButtonElement | null>(null) const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
const currentLevel = findCurrentSectionHeadingLevel(state) const currentLevel = useMemo(
() => findCurrentSectionHeadingLevel(state),
[state]
)
const currentLabel = currentLevel const currentLabel = currentLevel
? levels.get(currentLevel.level) ?? currentLevel.level ? levels.get(currentLevel.level) ?? currentLevel.level
: '---' : '---'
@ -64,52 +68,54 @@ export const SectionHeadingDropdown = () => {
<Icon type="caret-down" fw /> <Icon type="caret-down" fw />
</button> </button>
<Overlay {overflowOpen && (
show={overflowOpen} <Overlay
onHide={() => setOverflowOpen(false)} show
animation={false} onHide={() => setOverflowOpen(false)}
container={document.querySelector('.cm-editor')} animation={false}
containerPadding={0} container={view.dom}
placement="bottom" containerPadding={0}
rootClose placement="bottom"
target={toggleButtonRef.current ?? undefined} rootClose
> target={toggleButtonRef.current ?? undefined}
<Popover
id="popover-toolbar-section-heading"
className="ol-cm-toolbar-menu-popover"
> >
<div <Popover
className="ol-cm-toolbar-menu" id="popover-toolbar-section-heading"
id="section-heading-menu" className="ol-cm-toolbar-menu-popover"
role="menu"
aria-labelledby="section-heading-menu-button"
> >
{levelsEntries.map(([level, label]) => ( <div
<button className="ol-cm-toolbar-menu"
type="button" id="section-heading-menu"
role="menuitem" role="menu"
key={level} aria-labelledby="section-heading-menu-button"
onClick={() => { >
emitToolbarEvent(view, 'section-level-change') {levelsEntries.map(([level, label]) => (
setSectionHeadingLevel(view, level) <button
view.focus() type="button"
setOverflowOpen(false) role="menuitem"
}} key={level}
className={classnames( onClick={() => {
'ol-cm-toolbar-menu-item', emitToolbarEvent(view, 'section-level-change')
`section-level-${level}`, setSectionHeadingLevel(view, level)
{ view.focus()
'ol-cm-toolbar-menu-item-active': setOverflowOpen(false)
level === currentLevel?.level, }}
} className={classnames(
)} 'ol-cm-toolbar-menu-item',
> `section-level-${level}`,
{label} {
</button> 'ol-cm-toolbar-menu-item-active':
))} level === currentLevel?.level,
</div> }
</Popover> )}
</Overlay> >
{label}
</button>
))}
</div>
</Popover>
</Overlay>
)}
</> </>
) )
} }

View file

@ -4,10 +4,7 @@ import { EditorView } from '@codemirror/view'
import { useEditorContext } from '../../../../shared/context/editor-context' import { useEditorContext } from '../../../../shared/context/editor-context'
import useScopeEventEmitter from '../../../../shared/hooks/use-scope-event-emitter' import useScopeEventEmitter from '../../../../shared/hooks/use-scope-event-emitter'
import { useLayoutContext } from '../../../../shared/context/layout-context' import { useLayoutContext } from '../../../../shared/context/layout-context'
import { import { withinFormattingCommand } from '../../utils/tree-operations/ancestors'
minimumListDepthForSelection,
withinFormattingCommand,
} from '../../utils/tree-operations/ancestors'
import { ToolbarButton } from './toolbar-button' import { ToolbarButton } from './toolbar-button'
import { redo, undo } from '@codemirror/commands' import { redo, undo } from '@codemirror/commands'
import * as commands from '../../extensions/toolbar/commands' import * as commands from '../../extensions/toolbar/commands'
@ -25,11 +22,17 @@ export const ToolbarItems: FC<{
overflowed?: Set<string> overflowed?: Set<string>
languageName?: string languageName?: string
visual: boolean visual: boolean
}> = memo(function ToolbarItems({ state, overflowed, languageName, visual }) { listDepth: number
}> = memo(function ToolbarItems({
state,
overflowed,
languageName,
visual,
listDepth,
}) {
const { t } = useTranslation() const { t } = useTranslation()
const { toggleSymbolPalette, showSymbolPalette } = useEditorContext() const { toggleSymbolPalette, showSymbolPalette } = useEditorContext()
const isActive = withinFormattingCommand(state) const isActive = withinFormattingCommand(state)
const listDepth = minimumListDepthForSelection(state)
const addCommentEmitter = useScopeEventEmitter('comment:start_adding') const addCommentEmitter = useScopeEventEmitter('comment:start_adding')
const { setReviewPanelOpen } = useLayoutContext() const { setReviewPanelOpen } = useLayoutContext()
const splitTestVariants = getMeta('ol-splitTestVariants', {}) const splitTestVariants = getMeta('ol-splitTestVariants', {})