Преглед изворни кода

Merge pull request #21259 from overleaf/td-bs5-editor-search

[BS5] Migrate Editor search panel

GitOrigin-RevId: 37605845d4efc27d6c0c3a11de12387e8e3262f4
Tim Down пре 1 година
родитељ
комит
1b2385dfce

+ 139 - 127
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"
     >
       <div className="ol-cm-search-controls">
-        <InputGroup
-          bsSize="small"
+        <span
           className={classnames('ol-cm-search-input-group', {
             'ol-cm-search-input-error':
               query.regexp && isInvalidRegExp(query.search),
           })}
         >
-          <FormControl
+          <OLFormControl
+            ref={handleInputRef}
             type="text"
             name="search"
             // IMPORTANT: CodeMirror uses this attribute to focus the input
@@ -245,99 +250,90 @@ const CodeMirrorSearchForm: FC = () => {
             onChange={handleChange}
             onKeyDown={handleSearchKeyDown}
             className="ol-cm-search-form-input"
-            bsSize="small"
-            inputRef={handleInputRef}
+            size="sm"
             aria-label={t('search_command_find')}
           />
 
-          <InputGroup.Button>
-            <Tooltip
-              id="search-match-case"
-              description={t('search_match_case')}
+          <OLTooltip
+            id="search-match-case"
+            description={t('search_match_case')}
+          >
+            <label
+              className={classnames(
+                'btn btn-sm btn-default ol-cm-search-input-button',
+                {
+                  checked: query.caseSensitive,
+                  focused: activeSearchOption === 'caseSensitive',
+                }
+              )}
+              htmlFor={caseSensitiveId}
+              aria-label={t('search_match_case')}
             >
-              <label
-                className={classnames(
-                  'btn btn-sm btn-default ol-cm-search-input-button',
-                  {
-                    checked: query.caseSensitive,
-                    focused: activeSearchOption === 'caseSensitive',
-                  }
-                )}
-                htmlFor={caseSensitiveId}
-                aria-label={t('search_match_case')}
-              >
-                Aa
-              </label>
-            </Tooltip>
-          </InputGroup.Button>
-
-          <InputGroup.Button>
-            <Tooltip id="search-regexp" description={t('search_regexp')}>
-              <label
-                className={classnames(
-                  'btn btn-sm btn-default ol-cm-search-input-button',
-                  {
-                    checked: query.regexp,
-                    focused: activeSearchOption === 'regexp',
-                  }
-                )}
-                htmlFor={regexpId}
-                aria-label={t('search_regexp')}
-              >
-                [.*]
-              </label>
-            </Tooltip>
-          </InputGroup.Button>
-
-          <InputGroup.Button>
-            <Tooltip
-              id="search-whole-word"
-              description={t('search_whole_word')}
+              Aa
+            </label>
+          </OLTooltip>
+
+          <OLTooltip id="search-regexp" description={t('search_regexp')}>
+            <label
+              className={classnames(
+                'btn btn-sm btn-default ol-cm-search-input-button',
+                {
+                  checked: query.regexp,
+                  focused: activeSearchOption === 'regexp',
+                }
+              )}
+              htmlFor={regexpId}
+              aria-label={t('search_regexp')}
             >
-              <label
-                className={classnames(
-                  'btn btn-sm btn-default ol-cm-search-input-button',
-                  {
-                    checked: query.wholeWord,
-                    focused: activeSearchOption === 'wholeWord',
-                  }
-                )}
-                htmlFor={wholeWordId}
-                aria-label={t('search_whole_word')}
-              >
-                W
-              </label>
-            </Tooltip>
-          </InputGroup.Button>
-
-          <InputGroup.Button>
-            <Tooltip
-              id="search-within-selection"
-              description={t('search_within_selection')}
+              [.*]
+            </label>
+          </OLTooltip>
+
+          <OLTooltip
+            id="search-whole-word"
+            description={t('search_whole_word')}
+          >
+            <label
+              className={classnames(
+                'btn btn-sm btn-default ol-cm-search-input-button',
+                {
+                  checked: query.wholeWord,
+                  focused: activeSearchOption === 'wholeWord',
+                }
+              )}
+              htmlFor={wholeWordId}
+              aria-label={t('search_whole_word')}
+            >
+              W
+            </label>
+          </OLTooltip>
+          <OLTooltip
+            id="search-within-selection"
+            description={t('search_within_selection')}
+          >
+            <label
+              className={classnames(
+                'btn btn-sm btn-default ol-cm-search-input-button',
+                {
+                  checked: !!query.scope,
+                  focused: activeSearchOption === 'withinSelection',
+                }
+              )}
+              htmlFor={withinSelectionId}
+              aria-label={t('search_within_selection')}
             >
-              <label
-                className={classnames(
-                  'btn btn-sm btn-default ol-cm-search-input-button',
-                  {
-                    checked: !!query.scope,
-                    focused: activeSearchOption === 'withinSelection',
-                  }
-                )}
-                htmlFor={withinSelectionId}
-                aria-label={t('search_within_selection')}
-              >
-                <Icon type="align-left" fw />
-              </label>
-            </Tooltip>
-          </InputGroup.Button>
-        </InputGroup>
+              <BootstrapVersionSwitcher
+                bs3={<Icon type="align-left" fw />}
+                bs5={<MaterialIcon type="format_align_left" />}
+              />
+            </label>
+          </OLTooltip>
+        </span>
 
         {showReplace && (
-          <InputGroup
-            bsSize="small"
-            className="ol-cm-search-input-group ol-cm-search-replace-input"
-          >
-            <FormControl
+          <span className="ol-cm-search-input-group ol-cm-search-replace-input">
+            <OLFormControl
+              ref={handleReplaceRef}
               type="text"
               name="replace"
               placeholder={t('search_replace_with')}
@@ -346,11 +342,10 @@ const CodeMirrorSearchForm: FC = () => {
               onChange={handleChange}
               onKeyDown={handleReplaceKeyDown}
               className="ol-cm-search-form-input"
-              bsSize="small"
-              inputRef={handleReplaceRef}
+              size="sm"
               aria-label={t('search_command_replace')}
             />
-          </InputGroup>
+          </span>
         )}
 
         <div className="ol-cm-search-hidden-inputs">
@@ -404,27 +399,51 @@ const CodeMirrorSearchForm: FC = () => {
         </div>
 
         <div className="ol-cm-search-form-group ol-cm-search-next-previous">
-          <ButtonGroup className="ol-cm-search-form-button-group">
-            <Button
-              type="button"
-              bsSize="small"
+          <OLButtonGroup className="ol-cm-search-form-button-group">
+            <OLButton
+              variant="secondary"
+              size="sm"
               onClick={() => findPrevious(view)}
             >
-              <Icon
-                type="chevron-up"
-                fw
-                accessibilityLabel={t('search_previous')}
+              <BootstrapVersionSwitcher
+                bs3={
+                  <Icon
+                    type="chevron-up"
+                    fw
+                    accessibilityLabel={t('search_previous')}
+                  />
+                }
+                bs5={
+                  <MaterialIcon
+                    type="keyboard_arrow_up"
+                    accessibilityLabel={t('search_previous')}
+                  />
+                }
               />
-            </Button>
+            </OLButton>
 
-            <Button type="button" bsSize="small" onClick={() => findNext(view)}>
-              <Icon
-                type="chevron-down"
-                fw
-                accessibilityLabel={t('search_next')}
+            <OLButton
+              variant="secondary"
+              size="sm"
+              onClick={() => findNext(view)}
+            >
+              <BootstrapVersionSwitcher
+                bs3={
+                  <Icon
+                    type="chevron-down"
+                    fw
+                    accessibilityLabel={t('search_next')}
+                  />
+                }
+                bs5={
+                  <MaterialIcon
+                    type="keyboard_arrow_down"
+                    accessibilityLabel={t('search_next')}
+                  />
+                }
               />
-            </Button>
-          </ButtonGroup>
+            </OLButton>
+          </OLButtonGroup>
 
           {position !== null && (
             <div className="ol-cm-search-form-position">
@@ -437,36 +456,29 @@ const CodeMirrorSearchForm: FC = () => {
 
         {showReplace && (
           <div className="ol-cm-search-form-group ol-cm-search-replace-buttons">
-            <Button
-              type="button"
-              bsSize="small"
+            <OLButton
+              variant="secondary"
+              size="sm"
               onClick={() => replaceNext(view)}
             >
               {t('search_replace')}
-            </Button>
+            </OLButton>
 
-            <Button
-              type="button"
-              bsSize="small"
+            <OLButton
+              variant="secondary"
+              size="sm"
               onClick={() => replaceAll(view)}
             >
               {t('search_replace_all')}
-            </Button>
+            </OLButton>
           </div>
         )}
       </div>
 
       <div className="ol-cm-search-form-close">
-        <Tooltip id="search-close" description={<>{t('close')} (Esc)</>}>
-          <button
-            className="close"
-            onClick={() => closeSearchPanel(view)}
-            type="button"
-            aria-label={t('close')}
-          >
-            <span aria-hidden="true">&times;</span>
-          </button>
-        </Tooltip>
+        <OLTooltip id="search-close" description={<>{t('close')} (Esc)</>}>
+          <OLCloseButton onClick={() => closeSearchPanel(view)} />
+        </OLTooltip>
       </div>
     </form>
   )

+ 26 - 14
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,

+ 7 - 1
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(', '),

+ 4 - 0
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,
       })
     )
 

+ 35 - 0
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<HTMLButtonElement, CloseButtonProps>(
+  (props, ref) => {
+    const { t } = useTranslation()
+
+    const bs3CloseButtonProps: React.ButtonHTMLAttributes<HTMLButtonElement> = {
+      className: classNames('close', props.className),
+      onClick: props.onClick,
+      onMouseOver: props.onMouseOver,
+      onMouseOut: props.onMouseOut,
+
+      'aria-label': t('close'),
+    }
+
+    return (
+      <BootstrapVersionSwitcher
+        bs3={
+          <button {...bs3CloseButtonProps}>
+            <span aria-hidden="true">&times;</span>
+          </button>
+        }
+        bs5={<CloseButton ref={ref} {...props} />}
+      />
+    )
+  }
+)
+
+OLCloseButton.displayName = 'OLCloseButton'
+
+export default OLCloseButton

+ 15 - 8
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<HTMLInputElement, OLFormControlProps>(
   (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<HTMLInputElement, OLFormControlProps>(
       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,