Merge pull request #33932 from overleaf/mg-select-style

Replace text label with icon in "Select style" toolbar button

GitOrigin-RevId: 52b93a29db47e99609a90294e53abe1057a6c71d
This commit is contained in:
Malik Glossop
2026-05-27 13:43:47 +02:00
committed by Copybot
parent 984b8e3f4a
commit a47f6443f8
6 changed files with 76 additions and 185 deletions
@@ -2113,7 +2113,6 @@
"toolbar_bold": "",
"toolbar_bulleted_list": "",
"toolbar_change_editor_mode": "",
"toolbar_choose_section_heading_level": "",
"toolbar_code_visual_editor_switch": "",
"toolbar_decrease_indent": "",
"toolbar_editor": "",
@@ -2138,7 +2137,7 @@
"toolbar_numbered_list": "",
"toolbar_redo": "",
"toolbar_search_file": "",
"toolbar_select_style": "",
"toolbar_section_heading_level": "",
"toolbar_selected_projects": "",
"toolbar_selected_projects_management_actions": "",
"toolbar_selected_projects_remove": "",
@@ -15,6 +15,7 @@ export const ToolbarButtonMenu: FC<
label: string
icon: React.ReactNode
orientation?: 'vertical' | 'horizontal'
className?: string
disabled?: boolean
disablePopover?: boolean
altCommand?: (view: EditorView) => void
@@ -25,6 +26,7 @@ export const ToolbarButtonMenu: FC<
id,
label,
orientation = 'vertical',
className,
altCommand,
onToggle,
disabled,
@@ -35,6 +37,18 @@ export const ToolbarButtonMenu: FC<
const { open, onToggle: handleToggle, ref } = useDropdown()
const view = useCodeMirrorViewContext()
useEffect(() => {
if (!open) return
const onResize = () => {
handleToggle(false)
}
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
}, [open, handleToggle])
useEffect(() => {
if (disablePopover && open) {
handleToggle(false)
@@ -48,9 +62,12 @@ export const ToolbarButtonMenu: FC<
const button = (
<button
type="button"
className="ol-cm-toolbar-button"
className={classNames('ol-cm-toolbar-button', className)}
aria-label={label}
aria-disabled={disabled}
aria-haspopup="menu"
aria-expanded={open}
aria-controls={`${id}-menu`}
onMouseDown={event => {
event.preventDefault()
event.stopPropagation()
@@ -1,4 +1,3 @@
import classnames from 'classnames'
import {
useCodeMirrorStateContext,
useCodeMirrorViewContext,
@@ -7,15 +6,12 @@ import {
findCurrentSectionHeadingLevel,
setSectionHeadingLevel,
} from '../../extensions/toolbar/sections'
import { useCallback, useMemo, useRef } from 'react'
import OLOverlay from '@/shared/components/ol/ol-overlay'
import OLPopover from '@/shared/components/ol/ol-popover'
import useEventListener from '../../../../shared/hooks/use-event-listener'
import useDropdown from '../../../../shared/hooks/use-dropdown'
import { useMemo } from 'react'
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
import MaterialIcon from '@/shared/components/material-icon'
import { useTranslation } from 'react-i18next'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { ToolbarButtonMenu } from './button-menu'
import OLListGroupItem from '@/shared/components/ol/ol-list-group-item'
const levels = new Map([
['text', 'Normal text'],
@@ -33,107 +29,41 @@ export const SectionHeadingDropdown = () => {
const view = useCodeMirrorViewContext()
const { t } = useTranslation()
const { open: overflowOpen, onToggle: setOverflowOpen } = useDropdown()
useEventListener(
'resize',
useCallback(() => {
setOverflowOpen(false)
}, [setOverflowOpen])
)
const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
const currentLevel = useMemo(
() => findCurrentSectionHeadingLevel(state),
[state]
)
const currentLabel = currentLevel
? (levels.get(currentLevel.level) ?? currentLevel.level)
: '---'
return (
<>
<OLTooltip
id="section-heading-dropdown-tooltip"
description={t('toolbar_select_style')}
overlayProps={{ placement: 'bottom' }}
>
<button
ref={toggleButtonRef}
type="button"
id="section-heading-menu-button"
aria-haspopup="true"
aria-controls="section-heading-menu"
aria-label={t('toolbar_choose_section_heading_level')}
className="ol-cm-toolbar-menu-toggle"
onMouseDown={event => event.preventDefault()}
onClick={() => setOverflowOpen(!overflowOpen)}
>
<span>{currentLabel}</span>
<ToolbarButtonMenu
id="section-heading-menu-button"
label={t('toolbar_section_heading_level')}
className="ol-cm-toolbar-button-wide"
icon={
<>
<MaterialIcon
type="text_fields"
style={{ transform: 'scaleX(-1)' }}
/>
<MaterialIcon type="expand_more" />
</button>
</OLTooltip>
{overflowOpen && (
<OLOverlay
show
onHide={() => setOverflowOpen(false)}
transition={false}
container={view.dom}
containerPadding={0}
placement="bottom"
rootClose
target={toggleButtonRef.current}
popperConfig={{
modifiers: [
{
name: 'offset',
options: {
offset: [0, 1],
},
},
],
</>
}
>
{levelsEntries.map(([level, label]) => (
<OLListGroupItem
role="menuitem"
key={level}
active={level === currentLevel?.level}
onClick={() => {
emitToolbarEvent(view, 'section-level-change')
setSectionHeadingLevel(view, level)
view.focus()
}}
className={`ol-cm-section-heading-menu-item section-level-${level}`}
>
<OLPopover
id="popover-toolbar-section-heading"
className="ol-cm-toolbar-menu-popover"
>
<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>
</OLPopover>
</OLOverlay>
)}
</>
{label}
</OLListGroupItem>
))}
</ToolbarButtonMenu>
)
}
@@ -102,8 +102,29 @@ const toolbarTheme = EditorView.theme({
color: 'var(--toolbar-btn-color)',
borderColor: 'var(--editor-toolbar-bg)',
background: 'none',
'&.active': {
backgroundColor: 'rgba(125, 125, 125, 0.1)',
color: 'inherit',
},
'&:hover, &:focus': {
backgroundColor: 'rgba(125, 125, 125, 0.2)',
color: 'inherit',
},
'&.ol-cm-section-heading-menu-item': {
border: 'none',
padding: '4px 12px',
height: '40px',
fontSize: '14px',
fontWeight: 'bold',
},
'&.section-level-section': {
fontSize: '1.44em',
},
'&.section-level-subsection': {
fontSize: '1.2em',
},
'&.section-level-text': {
fontWeight: 'normal',
},
},
},
@@ -174,6 +195,9 @@ const toolbarTheme = EditorView.theme({
fontWeight: 700,
},
},
'.ol-cm-toolbar-button.ol-cm-toolbar-button-wide': {
width: 'auto',
},
'&.overall-theme-dark .ol-cm-toolbar-button': {
opacity: 0.8,
'&:hover, &:focus, &:active, &.active': {
@@ -198,84 +222,6 @@ const toolbarTheme = EditorView.theme({
display: 'flex',
},
},
'.ol-cm-toolbar-menu-toggle': {
background: 'transparent',
border: 'none',
color: 'inherit',
borderRadius: 'var(--border-radius-base)',
opacity: 0.8,
width: '120px',
fontSize: '13px',
fontWeight: '700',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '5px 6px',
'&:hover, &:focus, &.active': {
backgroundColor: 'rgba(125, 125, 125, 0.1)',
opacity: '1',
color: 'inherit',
},
'& .caret': {
marginTop: '0',
},
},
'.ol-cm-toolbar-menu-popover': {
border: 'none',
borderRadius: '0',
borderBottomLeftRadius: '4px',
borderBottomRightRadius: '4px',
boxShadow: '0 2px 5px rgb(0 0 0 / 20%)',
backgroundColor: 'var(--editor-toolbar-bg)',
color: 'var(--toolbar-btn-color)',
padding: '0',
'&.bottom': {
marginTop: '1px',
},
'&.top': {
marginBottom: '1px',
},
'& .arrow, & .popover-arrow': {
display: 'none',
},
'& .popover-content, & > .popover-body': {
padding: '0',
color: 'inherit',
},
'& .ol-cm-toolbar-menu': {
minWidth: '120px',
display: 'flex',
flexDirection: 'column',
boxSizing: 'border-box',
fontSize: '14px',
},
'& .ol-cm-toolbar-menu-item': {
border: 'none',
background: 'none',
padding: '4px 12px',
height: '40px',
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
color: 'inherit',
'&.ol-cm-toolbar-menu-item-active': {
backgroundColor: 'rgba(125, 125, 125, 0.1)',
},
'&:hover': {
backgroundColor: 'rgba(125, 125, 125, 0.2)',
color: 'inherit',
},
'&.section-level-section': {
fontSize: '1.44em',
},
'&.section-level-subsection': {
fontSize: '1.2em',
},
'&.section-level-body': {
fontWeight: 'normal',
},
},
},
'&.overall-theme-dark .ol-cm-toolbar-table-grid': {
'& td.active': {
outlineColor: 'white',
+1 -2
View File
@@ -2727,7 +2727,6 @@
"toolbar_bold": "Bold",
"toolbar_bulleted_list": "Bulleted list",
"toolbar_change_editor_mode": "Change editor mode: Code / Visual",
"toolbar_choose_section_heading_level": "Choose section heading level",
"toolbar_code_visual_editor_switch": "Code and visual editor switch",
"toolbar_decrease_indent": "Decrease indent",
"toolbar_editor": "Editor tools",
@@ -2752,7 +2751,7 @@
"toolbar_numbered_list": "Numbered list",
"toolbar_redo": "Redo",
"toolbar_search_file": "Search file",
"toolbar_select_style": "Select style",
"toolbar_section_heading_level": "Section heading level",
"toolbar_selected_projects": "Selected projects",
"toolbar_selected_projects_management_actions": "Selected projects management actions",
"toolbar_selected_projects_remove": "Remove selected projects",
@@ -68,14 +68,14 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
mountEditor('hi')
cy.get('.cm-content').should('have.text', 'hi')
clickToolbarButton('Choose section heading level')
clickToolbarButton('Section heading level')
cy.findByRole('menu').within(() => {
cy.findByText('Subsection').click()
})
cy.get('.cm-content').should('have.text', 'hi')
cy.get('.ol-cm-command-subsection').should('have.length', 1)
clickToolbarButton('Choose section heading level')
clickToolbarButton('Section heading level')
cy.findByRole('menu').within(() => {
cy.findByText('Normal text').click()
})