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

View file

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

View file

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

View file

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

View file

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

View file

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