diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 8a0dc6128e..4aee1c15c6 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx index b8c0893432..2f6b96151e 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx +++ b/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< } + orientation="horizontal" + > + + { + emitToolbarEvent(view, 'toolbar-bullet-list') + event.preventDefault() + toggleBulletList(view) + view.focus() + }} + > + + + + + { + emitToolbarEvent(view, 'toolbar-numbered-list') + event.preventDefault() + toggleNumberedList(view) + view.focus() + }} + > + + + + + ) +}) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx index fa65e9bded..9b7e518a65 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx +++ b/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')} > - - - - + + {listDepth >= 1 && ( + <> + + + + )} )} diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts index 098b28265f..5ee0498f52 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts +++ b/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', diff --git a/services/web/frontend/js/shared/components/ol/ol-list-group-item.tsx b/services/web/frontend/js/shared/components/ol/ol-list-group-item.tsx index c99b8c1cb4..35c279bbc8 100644 --- a/services/web/frontend/js/shared/components/ol/ol-list-group-item.tsx +++ b/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' +const OLListGroupItem = forwardRef( + function OLListGroupItem(props, ref) { + const as = props.as ?? 'button' - return ( - - ) -} + return ( + + ) + } +) + +OLListGroupItem.displayName = 'OLListGroupItem' export default OLListGroupItem diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 04103df886..b1d04fd50a 100644 --- a/services/web/locales/en.json +++ b/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", diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx index c3f75bdcdc..dc81924dcf 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx +++ b/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(' 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(' 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(' 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(' 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(' 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(' 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(' 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(' 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(' 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(' 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(' 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' + ) + }) })