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

View file

@ -257,21 +257,29 @@ export const search = () => {
const searchFormTheme = EditorView.theme({ const searchFormTheme = EditorView.theme({
'.ol-cm-search-form': { '.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', display: 'flex',
gap: '10px', gap: 'var(--ol-cm-search-form-gap)',
background: 'var(--ol-blue-gray-1)', background: 'var(--neutral-20)',
'--ol-cm-search-form-focus-shadow': '--ol-cm-search-form-focus-shadow':
'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%)', 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%)',
'--ol-cm-search-form-error-shadow': '--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', 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': { '.ol-cm-search-controls': {
display: 'grid', display: 'grid',
gridTemplateColumns: 'auto auto', gridTemplateColumns: 'auto auto',
gridTemplateRows: 'auto auto', gridTemplateRows: 'auto auto',
gap: '10px', gap: 'var(--ol-cm-search-form-gap)',
}, },
'@container (max-width: 450px)': { '@container (max-width: 450px)': {
'.ol-cm-search-controls': { '.ol-cm-search-controls': {
@ -280,12 +288,12 @@ const searchFormTheme = EditorView.theme({
}, },
'.ol-cm-search-form-row': { '.ol-cm-search-form-row': {
display: 'flex', display: 'flex',
gap: '10px', gap: 'var(--ol-cm-search-form-gap)',
justifyContent: 'space-between', justifyContent: 'space-between',
}, },
'.ol-cm-search-form-group': { '.ol-cm-search-form-group': {
display: 'flex', display: 'flex',
gap: '10px', gap: 'var(--ol-cm-search-form-gap)',
alignItems: 'center', alignItems: 'center',
}, },
'.ol-cm-search-input-group': { '.ol-cm-search-input-group': {
@ -294,6 +302,8 @@ const searchFormTheme = EditorView.theme({
background: 'white', background: 'white',
width: '100%', width: '100%',
maxWidth: '25em', maxWidth: '25em',
display: 'inline-flex',
alignItems: 'center',
'& input[type="text"]': { '& input[type="text"]': {
background: 'none', background: 'none',
boxShadow: 'none', boxShadow: 'none',
@ -303,18 +313,18 @@ const searchFormTheme = EditorView.theme({
boxShadow: 'none', boxShadow: 'none',
}, },
'& .btn.btn': { '& .btn.btn': {
background: 'var(--ol-blue-gray-0)', background: 'var(--neutral-10)',
color: 'var(--ol-blue-gray-3)', color: 'var(--neutral-60)',
borderRadius: '50%', borderRadius: '50%',
height: '2em', height: '2em',
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: '2em', width: '2em',
marginRight: '3px', marginRight: 'var(--ol-cm-search-form-button-margin)',
'&.checked': { '&.checked': {
color: '#fff', color: 'var(--white)',
backgroundColor: 'var(--ol-blue)', backgroundColor: 'var(--blue-50)',
}, },
'&:active': { '&:active': {
boxShadow: 'none', boxShadow: 'none',
@ -331,7 +341,7 @@ const searchFormTheme = EditorView.theme({
boxShadow: 'var(--ol-cm-search-form-error-shadow)', boxShadow: 'var(--ol-cm-search-form-error-shadow)',
}, },
}, },
'.input-group .ol-cm-search-form-input': { '.ol-cm-search-form-input': {
border: 'none', border: 'none',
}, },
'.ol-cm-search-input-button': { '.ol-cm-search-input-button': {
@ -355,7 +365,9 @@ const searchFormTheme = EditorView.theme({
left: '-10000px', left: '-10000px',
}, },
'.ol-cm-search-form-close': { '.ol-cm-search-form-close': {
flex: 1, marginLeft: 'auto',
display: 'flex',
alignItems: 'start',
}, },
'.ol-cm-search-replace-input': { '.ol-cm-search-replace-input': {
order: 3, order: 3,

View file

@ -2,6 +2,7 @@ import { EditorView } from '@codemirror/view'
import { Annotation, Compartment, TransactionSpec } from '@codemirror/state' import { Annotation, Compartment, TransactionSpec } from '@codemirror/state'
import { syntaxHighlighting } from '@codemirror/language' import { syntaxHighlighting } from '@codemirror/language'
import { classHighlighter } from './class-highlighter' import { classHighlighter } from './class-highlighter'
import classNames from 'classnames'
const optionsThemeConf = new Compartment() const optionsThemeConf = new Compartment()
const selectedThemeConf = new Compartment() const selectedThemeConf = new Compartment()
@ -16,6 +17,7 @@ type Options = {
fontFamily: FontFamily fontFamily: FontFamily
lineHeight: LineHeight lineHeight: LineHeight
overallTheme: OverallTheme overallTheme: OverallTheme
bootstrapVersion: 3 | 5
} }
export const theme = (options: Options) => [ export const theme = (options: Options) => [
@ -67,13 +69,17 @@ const createThemeFromOptions = ({
fontFamily = 'monaco', fontFamily = 'monaco',
lineHeight = 'normal', lineHeight = 'normal',
overallTheme = '', overallTheme = '',
bootstrapVersion = 3,
}: Options) => { }: Options) => {
/** /**
* Theme styles that depend on settings. * Theme styles that depend on settings.
*/ */
return [ return [
EditorView.editorAttributes.of({ 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({ style: Object.entries({
'--font-size': `${fontSize}px`, '--font-size': `${fontSize}px`,
'--source-font-family': fontFamilies[fontFamily]?.join(', '), '--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 { updateRanges } from '@/features/source-editor/extensions/ranges'
import { useThreadsContext } from '@/features/review-panel-new/context/threads-context' import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
import { useHunspell } from '@/features/source-editor/hooks/use-hunspell' import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
function useCodeMirrorScope(view: EditorView) { function useCodeMirrorScope(view: EditorView) {
const { fileTreeData } = useFileTreeData() const { fileTreeData } = useFileTreeData()
@ -141,6 +142,7 @@ function useCodeMirrorScope(view: EditorView) {
lineHeight, lineHeight,
overallTheme, overallTheme,
editorTheme, editorTheme,
bootstrapVersion: 3 as 3 | 5,
}) })
useEffect(() => { useEffect(() => {
@ -150,6 +152,7 @@ function useCodeMirrorScope(view: EditorView) {
lineHeight, lineHeight,
overallTheme, overallTheme,
editorTheme, editorTheme,
bootstrapVersion: isBootstrap5() ? 5 : 3,
} }
view.dispatch( view.dispatch(
@ -158,6 +161,7 @@ function useCodeMirrorScope(view: EditorView) {
fontSize, fontSize,
lineHeight, lineHeight,
overallTheme, 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 { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control' import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
import BS3FormControl from '@/features/ui/components/bootstrap-3/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) => { (props, ref) => {
const { bs3Props, ...rest } = props 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 = { let bs3FormControlProps: BS3FormControlProps = {
inputRef: bs3InputRef,
componentClass: rest.as, componentClass: rest.as,
bsSize: rest.size,
id: rest.id, id: rest.id,
name: rest.name, name: rest.name,
className: rest.className, className: rest.className,
@ -37,13 +51,6 @@ const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
onFocus: rest.onFocus as BS3FormControlProps['onFocus'], onFocus: rest.onFocus as BS3FormControlProps['onFocus'],
onBlur: rest.onBlur as BS3FormControlProps['onBlur'], onBlur: rest.onBlur as BS3FormControlProps['onBlur'],
onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'], onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'],
inputRef: (inputElement: HTMLInputElement) => {
if (typeof ref === 'function') {
ref(inputElement)
} else if (ref) {
ref.current = inputElement
}
},
prepend: rest.prepend, prepend: rest.prepend,
append: rest.append, append: rest.append,
...bs3Props, ...bs3Props,