Przeglądaj źródła

Merge pull request #33600 from overleaf/worktree-mg-bullet-list-compact

Combine toolbar list controls and render indentation controls conditionally

GitOrigin-RevId: 48d7c52983449566bfa21b5572915d79e595c704
Malik Glossop 2 miesięcy temu
rodzic
commit
8af5c2c346

+ 1 - 0
services/web/frontend/extracted-translations.json

@@ -2126,6 +2126,7 @@
   "toolbar_insert_figure": "",
   "toolbar_insert_inline_math": "",
   "toolbar_insert_link": "",
+  "toolbar_insert_list": "",
   "toolbar_insert_math": "",
   "toolbar_insert_math_and_symbols": "",
   "toolbar_insert_math_lowercase": "",

+ 7 - 1
services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx

@@ -7,12 +7,14 @@ import OLPopover from '@/shared/components/ol/ol-popover'
 import { EditorView } from '@codemirror/view'
 import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
 import { useCodeMirrorViewContext } from '../codemirror-context'
+import classNames from 'classnames'
 
 export const ToolbarButtonMenu: FC<
   React.PropsWithChildren<{
     id: string
     label: string
     icon: React.ReactNode
+    orientation?: 'vertical' | 'horizontal'
     disabled?: boolean
     disablePopover?: boolean
     altCommand?: (view: EditorView) => void
@@ -22,6 +24,7 @@ export const ToolbarButtonMenu: FC<
   icon,
   id,
   label,
+  orientation = 'vertical',
   altCommand,
   onToggle,
   disabled,
@@ -86,7 +89,10 @@ export const ToolbarButtonMenu: FC<
       <OLPopover
         id={`${id}-menu`}
         ref={ref}
-        className="ol-cm-toolbar-button-menu-popover"
+        className={classNames('ol-cm-toolbar-button-menu-popover', {
+          'ol-cm-toolbar-button-menu-popover-horizontal':
+            orientation === 'horizontal',
+        })}
       >
         <OLListGroup
           role="menu"

+ 61 - 0
services/web/frontend/js/features/source-editor/components/toolbar/insert-list-dropdown.tsx

@@ -0,0 +1,61 @@
+import { ToolbarButtonMenu } from './button-menu'
+import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
+import MaterialIcon from '../../../../shared/components/material-icon'
+import { useTranslation } from 'react-i18next'
+import { useCodeMirrorViewContext } from '../codemirror-context'
+import {
+  toggleBulletList,
+  toggleNumberedList,
+} from '../../extensions/toolbar/commands'
+import { memo } from 'react'
+import OLListGroupItem from '@/shared/components/ol/ol-list-group-item'
+import OLTooltip from '@/shared/components/ol/ol-tooltip'
+
+export const InsertListDropdown = memo(function InsertListDropdown() {
+  const { t } = useTranslation()
+  const view = useCodeMirrorViewContext()
+
+  return (
+    <ToolbarButtonMenu
+      id="toolbar-insert-list"
+      label={t('toolbar_insert_list')}
+      icon={<MaterialIcon type="format_list_bulleted" />}
+      orientation="horizontal"
+    >
+      <OLTooltip
+        id="toolbar-bullet-list"
+        description={t('toolbar_bulleted_list')}
+        overlayProps={{ placement: 'bottom' }}
+      >
+        <OLListGroupItem
+          aria-label={t('toolbar_bulleted_list')}
+          onClick={event => {
+            emitToolbarEvent(view, 'toolbar-bullet-list')
+            event.preventDefault()
+            toggleBulletList(view)
+            view.focus()
+          }}
+        >
+          <MaterialIcon type="format_list_bulleted" />
+        </OLListGroupItem>
+      </OLTooltip>
+      <OLTooltip
+        id="toolbar-numbered-list"
+        description={t('toolbar_numbered_list')}
+        overlayProps={{ placement: 'bottom' }}
+      >
+        <OLListGroupItem
+          aria-label={t('toolbar_numbered_list')}
+          onClick={event => {
+            emitToolbarEvent(view, 'toolbar-numbered-list')
+            event.preventDefault()
+            toggleNumberedList(view)
+            view.focus()
+          }}
+        >
+          <MaterialIcon type="format_list_numbered" />
+        </OLListGroupItem>
+      </OLTooltip>
+    </ToolbarButtonMenu>
+  )
+})

+ 21 - 28
services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx

@@ -9,6 +9,7 @@ import getMeta from '../../../../utils/meta'
 import { InsertFigureDropdown } from './insert-figure-dropdown'
 import { useTranslation } from 'react-i18next'
 import { MathDropdown } from './math-dropdown'
+import { InsertListDropdown } from './insert-list-dropdown'
 import { TableDropdown } from './table-dropdown'
 import { LegacyTableDropdown } from './table-inserter-dropdown-legacy'
 import { withinFormattingCommand } from '@/features/source-editor/utils/tree-operations/formatting'
@@ -164,34 +165,26 @@ export const ToolbarItems: FC<{
               data-overflow="group-list"
               aria-label={t('toolbar_list_indentation')}
             >
-              <ToolbarButton
-                id="toolbar-bullet-list"
-                label={t('toolbar_bulleted_list')}
-                command={commands.toggleBulletList}
-                icon="format_list_bulleted"
-              />
-              <ToolbarButton
-                id="toolbar-numbered-list"
-                label={t('toolbar_numbered_list')}
-                command={commands.toggleNumberedList}
-                icon="format_list_numbered"
-              />
-              <ToolbarButton
-                id="toolbar-format-indent-decrease"
-                label={t('toolbar_decrease_indent')}
-                command={commands.indentDecrease}
-                icon="format_indent_decrease"
-                shortcut={visual ? (isMac ? '⌘[' : 'Ctrl+[') : undefined}
-                disabled={listDepth < 2}
-              />
-              <ToolbarButton
-                id="toolbar-format-indent-increase"
-                label={t('toolbar_increase_indent')}
-                command={commands.indentIncrease}
-                icon="format_indent_increase"
-                shortcut={visual ? (isMac ? '⌘]' : 'Ctrl+]') : undefined}
-                disabled={listDepth < 1}
-              />
+              <InsertListDropdown />
+              {listDepth >= 1 && (
+                <>
+                  <ToolbarButton
+                    id="toolbar-format-indent-decrease"
+                    label={t('toolbar_decrease_indent')}
+                    command={commands.indentDecrease}
+                    icon="format_indent_decrease"
+                    shortcut={visual ? (isMac ? '⌘[' : 'Ctrl+[') : undefined}
+                    disabled={listDepth < 2}
+                  />
+                  <ToolbarButton
+                    id="toolbar-format-indent-increase"
+                    label={t('toolbar_increase_indent')}
+                    command={commands.indentIncrease}
+                    icon="format_indent_increase"
+                    shortcut={visual ? (isMac ? '⌘]' : 'Ctrl+]') : undefined}
+                  />
+                </>
+              )}
             </div>
           )}
         </>

+ 5 - 0
services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts

@@ -107,6 +107,11 @@ const toolbarTheme = EditorView.theme({
       },
     },
   },
+  '.ol-cm-toolbar-button-menu-popover-horizontal': {
+    '& .list-group': {
+      flexDirection: 'row',
+    },
+  },
   '.ol-cm-toolbar-button-group': {
     display: 'flex',
     alignItems: 'center',

+ 17 - 11
services/web/frontend/js/shared/components/ol/ol-list-group-item.tsx

@@ -1,15 +1,21 @@
+import { forwardRef } from 'react'
 import { ListGroupItem, ListGroupItemProps } from 'react-bootstrap'
 
-function OLListGroupItem(props: ListGroupItemProps) {
-  const as = props.as ?? 'button'
-
-  return (
-    <ListGroupItem
-      {...props}
-      as={as}
-      type={as === 'button' ? 'button' : undefined}
-    />
-  )
-}
+const OLListGroupItem = forwardRef<HTMLElement, ListGroupItemProps>(
+  function OLListGroupItem(props, ref) {
+    const as = props.as ?? 'button'
+
+    return (
+      <ListGroupItem
+        {...props}
+        ref={ref}
+        as={as}
+        type={as === 'button' ? 'button' : undefined}
+      />
+    )
+  }
+)
+
+OLListGroupItem.displayName = 'OLListGroupItem'
 
 export default OLListGroupItem

+ 1 - 0
services/web/locales/en.json

@@ -2740,6 +2740,7 @@
   "toolbar_insert_figure": "Insert figure",
   "toolbar_insert_inline_math": "Insert inline math",
   "toolbar_insert_link": "Insert link",
+  "toolbar_insert_list": "Insert list",
   "toolbar_insert_math": "Insert math",
   "toolbar_insert_math_and_symbols": "Insert math and symbols",
   "toolbar_insert_math_lowercase": "Insert math",

+ 61 - 18
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx

@@ -19,6 +19,11 @@ const clickToolbarButton = (name: string) => {
   cy.findByRole('button', { name }).trigger('mouseout')
 }
 
+const clickListType = (type: string) => {
+  cy.findByRole('button', { name: 'Insert list' }).click()
+  cy.findByRole('button', { name: type }).click()
+}
+
 const mountEditor = (content: string) => {
   const scope = mockScope(content)
 
@@ -131,8 +136,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
     mountEditor('test')
     selectAll()
 
-    clickToolbarButton('More editor toolbar items')
-    clickToolbarButton('Bulleted list')
+    clickListType('Bulleted list')
 
     cy.get('.cm-content').should('have.text', ' test')
 
@@ -144,8 +148,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
     mountEditor('test')
     selectAll()
 
-    clickToolbarButton('More editor toolbar items')
-    clickToolbarButton('Numbered list')
+    clickListType('Numbered list')
 
     cy.get('.cm-content').should('have.text', ' test')
 
@@ -157,8 +160,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
     mountEditor('test')
     selectAll()
 
-    clickToolbarButton('More editor toolbar items')
-    clickToolbarButton('Numbered list')
+    clickListType('Numbered list')
 
     // expose the markup
     cy.get('.cm-line').eq(0).type('{rightArrow}')
@@ -173,7 +175,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
       ].join('')
     )
 
-    clickToolbarButton('Bulleted list')
+    clickListType('Bulleted list')
 
     cy.get('.cm-content').should(
       'have.text',
@@ -190,8 +192,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
     mountEditor('test')
     selectAll()
 
-    clickToolbarButton('More editor toolbar items')
-    clickToolbarButton('Numbered list')
+    clickListType('Numbered list')
 
     // expose the markup
     cy.get('.cm-line').eq(0).type('{rightArrow}')
@@ -206,7 +207,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
       ].join('')
     )
 
-    clickToolbarButton('Numbered list')
+    clickListType('Numbered list')
 
     cy.get('.cm-content').should('have.text', 'test')
   })
@@ -215,8 +216,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
     mountEditor('test\ntest')
     selectAll()
 
-    clickToolbarButton('More editor toolbar items')
-    clickToolbarButton('Numbered list')
+    clickListType('Numbered list')
 
     // expose the markup
     cy.get('.cm-line').eq(1).type('{rightArrow}')
@@ -252,8 +252,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
 
     cy.get('.cm-line').eq(1).click()
 
-    clickToolbarButton('More editor toolbar items')
-    clickToolbarButton('Numbered list')
+    clickListType('Numbered list')
 
     cy.get('.cm-line').eq(0).type('{upArrow}')
 
@@ -273,8 +272,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
     mountEditor('test\ntest')
     selectAll()
 
-    clickToolbarButton('More editor toolbar items')
-    clickToolbarButton('Numbered list')
+    clickListType('Numbered list')
 
     // expose the markup
     cy.get('.cm-line').eq(1).type('{rightArrow}')
@@ -310,8 +308,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
 
     cy.get('.cm-line').eq(0).click()
 
-    clickToolbarButton('More editor toolbar items')
-    clickToolbarButton('Numbered list')
+    clickListType('Numbered list')
 
     // expose the markup
     cy.get('.cm-line').eq(1).type('{rightArrow}')
@@ -339,4 +336,50 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
     mountEditor('')
     cy.findByLabelText('Insert symbol').should('not.exist')
   })
+
+  it('should show both list type options in the list type dropdown', function () {
+    mountEditor('test')
+
+    cy.findByRole('button', { name: 'Insert list' }).click()
+    cy.findByRole('button', { name: 'Bulleted list' }).should('exist')
+    cy.findByRole('button', { name: 'Numbered list' }).should('exist')
+  })
+
+  it('should hide indent buttons when cursor is not in a list', function () {
+    mountEditor('test')
+
+    cy.findByRole('button', { name: 'Increase indent' }).should('not.exist')
+    cy.findByRole('button', { name: 'Decrease indent' }).should('not.exist')
+  })
+
+  it('should show indent buttons when cursor is inside a list', function () {
+    mountEditor('test')
+    selectAll()
+
+    clickListType('Bulleted list')
+
+    cy.get('.cm-line').eq(0).click()
+
+    cy.findByRole('button', { name: 'Increase indent' }).should('exist')
+    cy.findByRole('button', { name: 'Decrease indent' }).should('exist')
+  })
+
+  it('should disable decrease indent when at the top level of a list', function () {
+    mountEditor('test')
+    selectAll()
+
+    clickListType('Bulleted list')
+
+    cy.get('.cm-line').eq(0).click()
+
+    cy.findByRole('button', { name: 'Decrease indent' }).should(
+      'have.attr',
+      'aria-disabled',
+      'true'
+    )
+    cy.findByRole('button', { name: 'Increase indent' }).should(
+      'not.have.attr',
+      'aria-disabled'
+    )
+  })
 })