From 1b2385dfcec8c7b3c287f8163486b5a44b4cef63 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:07:26 +0100 Subject: [PATCH] Merge pull request #21259 from overleaf/td-bs5-editor-search [BS5] Migrate Editor search panel GitOrigin-RevId: 37605845d4efc27d6c0c3a11de12387e8e3262f4 --- .../components/codemirror-search-form.tsx | 262 +++++++++--------- .../source-editor/extensions/search.ts | 40 ++- .../source-editor/extensions/theme.ts | 8 +- .../hooks/use-codemirror-scope.ts | 4 + .../ui/components/ol/ol-close-button.tsx | 35 +++ .../ui/components/ol/ol-form-control.tsx | 23 +- 6 files changed, 224 insertions(+), 148 deletions(-) create mode 100644 services/web/frontend/js/features/ui/components/ol/ol-close-button.tsx diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx index 8dd3d36373..570f9cd58e 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx @@ -15,9 +15,14 @@ import { getSearchQuery, SearchCursor, } from '@codemirror/search' -import { Button, ButtonGroup, FormControl, InputGroup } from 'react-bootstrap' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import OLButton from '@/features/ui/components/ol/ol-button' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import MaterialIcon from '@/shared/components/material-icon' +import OLButtonGroup from '@/features/ui/components/ol/ol-button-group' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' +import OLCloseButton from '@/features/ui/components/ol/ol-close-button' import { useTranslation } from 'react-i18next' -import Tooltip from '../../../shared/components/tooltip' import Icon from '../../../shared/components/icon' import classnames from 'classnames' import { useUserSettingsContext } from '@/shared/context/user-settings-context' @@ -226,14 +231,14 @@ const CodeMirrorSearchForm: FC = () => { role="search" >
- - { onChange={handleChange} onKeyDown={handleSearchKeyDown} className="ol-cm-search-form-input" - bsSize="small" - inputRef={handleInputRef} + size="sm" aria-label={t('search_command_find')} /> - - + - + Aa + + - - - - - - - - + - + [.*] + + - - + - - + W + + + + + + {showReplace && ( - - + { onChange={handleChange} onKeyDown={handleReplaceKeyDown} className="ol-cm-search-form-input" - bsSize="small" - inputRef={handleReplaceRef} + size="sm" aria-label={t('search_command_replace')} /> - + )}
@@ -404,27 +399,51 @@ const CodeMirrorSearchForm: FC = () => {
- - + - - + + {position !== null && (
@@ -437,36 +456,29 @@ const CodeMirrorSearchForm: FC = () => { {showReplace && (
- + - +
)}
- {t('close')} (Esc)}> - - + {t('close')} (Esc)}> + closeSearchPanel(view)} /> +
) diff --git a/services/web/frontend/js/features/source-editor/extensions/search.ts b/services/web/frontend/js/features/source-editor/extensions/search.ts index 1a5d15c4c4..c27dd3692d 100644 --- a/services/web/frontend/js/features/source-editor/extensions/search.ts +++ b/services/web/frontend/js/features/source-editor/extensions/search.ts @@ -257,21 +257,29 @@ export const search = () => { const searchFormTheme = EditorView.theme({ '.ol-cm-search-form': { - padding: '10px', + '--ol-cm-search-form-gap': '10px', + '--ol-cm-search-form-button-margin': '3px', + padding: 'var(--ol-cm-search-form-gap)', display: 'flex', - gap: '10px', - background: 'var(--ol-blue-gray-1)', + gap: 'var(--ol-cm-search-form-gap)', + background: 'var(--neutral-20)', '--ol-cm-search-form-focus-shadow': 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%)', '--ol-cm-search-form-error-shadow': - 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px var(--input-shadow-danger-color)', + 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px var(--red-50)', containerType: 'inline-size', }, + '&.bootstrap-5 .ol-cm-search-form': { + '--ol-cm-search-form-gap': 'var(--spacing-05)', + '--ol-cm-search-form-button-margin': 'var(--spacing-02)', + '--input-border': 'var(--border-primary)', + '--input-border-focus': 'var(--border-active)', + }, '.ol-cm-search-controls': { display: 'grid', gridTemplateColumns: 'auto auto', gridTemplateRows: 'auto auto', - gap: '10px', + gap: 'var(--ol-cm-search-form-gap)', }, '@container (max-width: 450px)': { '.ol-cm-search-controls': { @@ -280,12 +288,12 @@ const searchFormTheme = EditorView.theme({ }, '.ol-cm-search-form-row': { display: 'flex', - gap: '10px', + gap: 'var(--ol-cm-search-form-gap)', justifyContent: 'space-between', }, '.ol-cm-search-form-group': { display: 'flex', - gap: '10px', + gap: 'var(--ol-cm-search-form-gap)', alignItems: 'center', }, '.ol-cm-search-input-group': { @@ -294,6 +302,8 @@ const searchFormTheme = EditorView.theme({ background: 'white', width: '100%', maxWidth: '25em', + display: 'inline-flex', + alignItems: 'center', '& input[type="text"]': { background: 'none', boxShadow: 'none', @@ -303,18 +313,18 @@ const searchFormTheme = EditorView.theme({ boxShadow: 'none', }, '& .btn.btn': { - background: 'var(--ol-blue-gray-0)', - color: 'var(--ol-blue-gray-3)', + background: 'var(--neutral-10)', + color: 'var(--neutral-60)', borderRadius: '50%', height: '2em', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '2em', - marginRight: '3px', + marginRight: 'var(--ol-cm-search-form-button-margin)', '&.checked': { - color: '#fff', - backgroundColor: 'var(--ol-blue)', + color: 'var(--white)', + backgroundColor: 'var(--blue-50)', }, '&:active': { boxShadow: 'none', @@ -331,7 +341,7 @@ const searchFormTheme = EditorView.theme({ boxShadow: 'var(--ol-cm-search-form-error-shadow)', }, }, - '.input-group .ol-cm-search-form-input': { + '.ol-cm-search-form-input': { border: 'none', }, '.ol-cm-search-input-button': { @@ -355,7 +365,9 @@ const searchFormTheme = EditorView.theme({ left: '-10000px', }, '.ol-cm-search-form-close': { - flex: 1, + marginLeft: 'auto', + display: 'flex', + alignItems: 'start', }, '.ol-cm-search-replace-input': { order: 3, diff --git a/services/web/frontend/js/features/source-editor/extensions/theme.ts b/services/web/frontend/js/features/source-editor/extensions/theme.ts index 105bb9efd0..4988743f00 100644 --- a/services/web/frontend/js/features/source-editor/extensions/theme.ts +++ b/services/web/frontend/js/features/source-editor/extensions/theme.ts @@ -2,6 +2,7 @@ import { EditorView } from '@codemirror/view' import { Annotation, Compartment, TransactionSpec } from '@codemirror/state' import { syntaxHighlighting } from '@codemirror/language' import { classHighlighter } from './class-highlighter' +import classNames from 'classnames' const optionsThemeConf = new Compartment() const selectedThemeConf = new Compartment() @@ -16,6 +17,7 @@ type Options = { fontFamily: FontFamily lineHeight: LineHeight overallTheme: OverallTheme + bootstrapVersion: 3 | 5 } export const theme = (options: Options) => [ @@ -67,13 +69,17 @@ const createThemeFromOptions = ({ fontFamily = 'monaco', lineHeight = 'normal', overallTheme = '', + bootstrapVersion = 3, }: Options) => { /** * Theme styles that depend on settings. */ return [ EditorView.editorAttributes.of({ - class: overallTheme === '' ? 'overall-theme-dark' : 'overall-theme-light', + class: classNames( + overallTheme === '' ? 'overall-theme-dark' : 'overall-theme-light', + 'bootstrap-' + bootstrapVersion + ), style: Object.entries({ '--font-size': `${fontSize}px`, '--source-font-family': fontFamilies[fontFamily]?.join(', '), diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index d44be37b3b..5ee52286bd 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts @@ -66,6 +66,7 @@ import { useRangesContext } from '@/features/review-panel-new/context/ranges-con import { updateRanges } from '@/features/source-editor/extensions/ranges' import { useThreadsContext } from '@/features/review-panel-new/context/threads-context' import { useHunspell } from '@/features/source-editor/hooks/use-hunspell' +import { isBootstrap5 } from '@/features/utils/bootstrap-5' function useCodeMirrorScope(view: EditorView) { const { fileTreeData } = useFileTreeData() @@ -141,6 +142,7 @@ function useCodeMirrorScope(view: EditorView) { lineHeight, overallTheme, editorTheme, + bootstrapVersion: 3 as 3 | 5, }) useEffect(() => { @@ -150,6 +152,7 @@ function useCodeMirrorScope(view: EditorView) { lineHeight, overallTheme, editorTheme, + bootstrapVersion: isBootstrap5() ? 5 : 3, } view.dispatch( @@ -158,6 +161,7 @@ function useCodeMirrorScope(view: EditorView) { fontSize, lineHeight, overallTheme, + bootstrapVersion: themeRef.current.bootstrapVersion, }) ) diff --git a/services/web/frontend/js/features/ui/components/ol/ol-close-button.tsx b/services/web/frontend/js/features/ui/components/ol/ol-close-button.tsx new file mode 100644 index 0000000000..7034392949 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/ol-close-button.tsx @@ -0,0 +1,35 @@ +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { CloseButton, CloseButtonProps } from 'react-bootstrap-5' +import classNames from 'classnames' +import { useTranslation } from 'react-i18next' +import { forwardRef } from 'react' + +const OLCloseButton = forwardRef( + (props, ref) => { + const { t } = useTranslation() + + const bs3CloseButtonProps: React.ButtonHTMLAttributes = { + className: classNames('close', props.className), + onClick: props.onClick, + onMouseOver: props.onMouseOver, + onMouseOut: props.onMouseOut, + + 'aria-label': t('close'), + } + + return ( + + + + } + bs5={} + /> + ) + } +) + +OLCloseButton.displayName = 'OLCloseButton' + +export default OLCloseButton diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx index 1b9ea18ed8..3aba265cac 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx @@ -1,4 +1,4 @@ -import { forwardRef, ComponentProps } from 'react' +import { forwardRef, ComponentProps, useCallback } from 'react' import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' import FormControl from '@/features/ui/components/bootstrap-5/form/form-control' import BS3FormControl from '@/features/ui/components/bootstrap-3/form/form-control' @@ -15,8 +15,22 @@ const OLFormControl = forwardRef( (props, ref) => { const { bs3Props, ...rest } = props + // Use a callback so that the ref passed to the BS3 FormControl is stable + const bs3InputRef = useCallback( + (inputElement: HTMLInputElement) => { + if (typeof ref === 'function') { + ref(inputElement) + } else if (ref) { + ref.current = inputElement + } + }, + [ref] + ) + let bs3FormControlProps: BS3FormControlProps = { + inputRef: bs3InputRef, componentClass: rest.as, + bsSize: rest.size, id: rest.id, name: rest.name, className: rest.className, @@ -37,13 +51,6 @@ const OLFormControl = forwardRef( onFocus: rest.onFocus as BS3FormControlProps['onFocus'], onBlur: rest.onBlur as BS3FormControlProps['onBlur'], onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'], - inputRef: (inputElement: HTMLInputElement) => { - if (typeof ref === 'function') { - ref(inputElement) - } else if (ref) { - ref.current = inputElement - } - }, prepend: rest.prepend, append: rest.append, ...bs3Props,