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

[BS5] Migrate Editor search panel

GitOrigin-RevId: 37605845d4efc27d6c0c3a11de12387e8e3262f4
This commit is contained in:
Tim Down 2024-10-23 15:07:26 +01:00 committed by Copybot
parent 2646fefce4
commit 1b2385dfce
6 changed files with 224 additions and 148 deletions

View file

@ -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,13 +250,11 @@ 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
<OLTooltip
id="search-match-case"
description={t('search_match_case')}
>
@ -268,11 +271,9 @@ const CodeMirrorSearchForm: FC = () => {
>
Aa
</label>
</Tooltip>
</InputGroup.Button>
</OLTooltip>
<InputGroup.Button>
<Tooltip id="search-regexp" description={t('search_regexp')}>
<OLTooltip id="search-regexp" description={t('search_regexp')}>
<label
className={classnames(
'btn btn-sm btn-default ol-cm-search-input-button',
@ -286,11 +287,9 @@ const CodeMirrorSearchForm: FC = () => {
>
[.*]
</label>
</Tooltip>
</InputGroup.Button>
</OLTooltip>
<InputGroup.Button>
<Tooltip
<OLTooltip
id="search-whole-word"
description={t('search_whole_word')}
>
@ -307,11 +306,8 @@ const CodeMirrorSearchForm: FC = () => {
>
W
</label>
</Tooltip>
</InputGroup.Button>
<InputGroup.Button>
<Tooltip
</OLTooltip>
<OLTooltip
id="search-within-selection"
description={t('search_within_selection')}
>
@ -326,18 +322,18 @@ const CodeMirrorSearchForm: FC = () => {
htmlFor={withinSelectionId}
aria-label={t('search_within_selection')}
>
<Icon type="align-left" fw />
<BootstrapVersionSwitcher
bs3={<Icon type="align-left" fw />}
bs5={<MaterialIcon type="format_align_left" />}
/>
</label>
</Tooltip>
</InputGroup.Button>
</InputGroup>
</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)}
>
<BootstrapVersionSwitcher
bs3={
<Icon
type="chevron-up"
fw
accessibilityLabel={t('search_previous')}
/>
</Button>
}
bs5={
<MaterialIcon
type="keyboard_arrow_up"
accessibilityLabel={t('search_previous')}
/>
}
/>
</OLButton>
<Button type="button" bsSize="small" onClick={() => findNext(view)}>
<OLButton
variant="secondary"
size="sm"
onClick={() => findNext(view)}
>
<BootstrapVersionSwitcher
bs3={
<Icon
type="chevron-down"
fw
accessibilityLabel={t('search_next')}
/>
</Button>
</ButtonGroup>
}
bs5={
<MaterialIcon
type="keyboard_arrow_down"
accessibilityLabel={t('search_next')}
/>
}
/>
</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>
)

View file

@ -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,

View file

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

View file

@ -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,
})
)

View file

@ -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

View file

@ -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,