[web] Spelling correction Dropdown to BS5 (#21493)

* Split `SpellingSuggestions` into a BS3 and BS5 version

* Migrate `B5SpellingSuggestions` to BS5

* Add `.dropdown-menu.dropdown-menu-unpositioned` styles

This makes the dropdown position itself without overflows

* Make spelling tooltip background transparent

* Migrate Cog icon to BS5

* Use `PolymorphicComponent`

Co-authored-by: Ilkin Ismailov <ilkin.ismailov@overleaf.com>

* Fix formatting

---------

Co-authored-by: Ilkin Ismailov <ilkin.ismailov@overleaf.com>
GitOrigin-RevId: aaa6c589637971031d13ac099f935fe2052e6989
This commit is contained in:
Antoine Clausse 2024-11-04 10:08:43 +01:00 committed by Copybot
parent d4fa37b75d
commit 053831b48c
5 changed files with 200 additions and 39 deletions

View file

@ -61,6 +61,7 @@ const spellingTheme = EditorView.baseTheme({
}, },
'.cm-tooltip.ol-cm-spelling-context-menu-tooltip': { '.cm-tooltip.ol-cm-spelling-context-menu-tooltip': {
borderWidth: '0', borderWidth: '0',
background: 'transparent',
}, },
}) })

View file

@ -4,10 +4,12 @@ import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import classnames from 'classnames' import classnames from 'classnames'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
import { Dropdown } from 'react-bootstrap-5'
import { bsVersion, isBootstrap5 } from '@/features/utils/bootstrap-5'
import PolymorphicComponent from '@/shared/components/polymorphic-component'
const SpellingSuggestionsFeedback: FC = () => { const SpellingSuggestionsFeedback: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<OLTooltip <OLTooltip
id="spell-check-client-tooltip" id="spell-check-client-tooltip"
@ -21,27 +23,31 @@ const SpellingSuggestionsFeedback: FC = () => {
tooltipProps={{ className: 'split-test-badge-tooltip' }} tooltipProps={{ className: 'split-test-badge-tooltip' }}
overlayProps={{ placement: 'bottom', delay: 100 }} overlayProps={{ placement: 'bottom', delay: 100 }}
> >
<a <span>
href="https://docs.google.com/forms/d/e/1FAIpQLSdD1wa5SiCZ7x_UF6e8vywTN82kSm6ou2rTKz-XBiEjNilOXQ/viewform" <PolymorphicComponent
target="_blank" as={isBootstrap5() ? Dropdown.Item : 'a'}
rel="noopener noreferrer" href="https://docs.google.com/forms/d/e/1FAIpQLSdD1wa5SiCZ7x_UF6e8vywTN82kSm6ou2rTKz-XBiEjNilOXQ/viewform"
> target="_blank"
<BootstrapVersionSwitcher rel="noopener noreferrer"
bs3={ className={bsVersion({ bs3: 'dropdown-menu-button' })}
<span >
className={classnames('badge', 'info-badge')} <BootstrapVersionSwitcher
style={{ width: 14, height: 14 }} bs3={
/> <span
} className={classnames('badge', 'info-badge')}
bs5={ style={{ width: 14, height: 14 }}
<MaterialIcon />
type="info" }
className={classnames('align-middle', 'info-badge')} bs5={
/> <MaterialIcon
} type="info"
/> className={classnames('align-middle', 'info-badge')}
<span className="mx-2">{t('give_feedback')}</span> />
</a> }
/>
<span className="mx-2">{t('give_feedback')}</span>
</PolymorphicComponent>
</span>
</OLTooltip> </OLTooltip>
) )
} }

View file

@ -2,6 +2,11 @@ import { memo, useCallback } from 'react'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip' import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import { bsVersion, isBootstrap5 } from '@/features/utils/bootstrap-5'
import { Dropdown } from 'react-bootstrap-5'
import PolymorphicComponent from '@/shared/components/polymorphic-component'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
export const SpellingSuggestionsLanguage = memo<{ export const SpellingSuggestionsLanguage = memo<{
language: { name: string } language: { name: string }
@ -27,12 +32,22 @@ export const SpellingSuggestionsLanguage = memo<{
description={t('change_language')} description={t('change_language')}
overlayProps={{ placement: 'right', delay: 100 }} overlayProps={{ placement: 'right', delay: 100 }}
> >
<button <span>
className="btn-link text-left dropdown-menu-button" <PolymorphicComponent
onClick={handleClick} as={isBootstrap5() ? Dropdown.Item : 'a'}
> className={bsVersion({
<Icon type="cog" /> <span className="mx-1">{language.name}</span> bs3: 'btn-link text-left dropdown-menu-button',
</button> bs5: 'd-flex gap-2 align-items-center',
})}
onClick={handleClick}
>
<BootstrapVersionSwitcher
bs3={<Icon type="cog" />}
bs5={<MaterialIcon type="settings" />}
/>
<span className={bsVersion({ bs3: 'ms-1' })}>{language.name}</span>
</PolymorphicComponent>
</span>
</OLTooltip> </OLTooltip>
) )
}) })

View file

@ -15,6 +15,9 @@ import SpellingSuggestionsFeedback from './spelling-suggestions-feedback'
import { SpellingSuggestionsLanguage } from './spelling-suggestions-language' import { SpellingSuggestionsLanguage } from './spelling-suggestions-language'
import { captureException } from '@/infrastructure/error-reporter' import { captureException } from '@/infrastructure/error-reporter'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { SpellCheckLanguage } from '../../../../../../types/project-settings'
import { Dropdown } from 'react-bootstrap-5'
const ITEMS_TO_SHOW = 8 const ITEMS_TO_SHOW = 8
@ -22,14 +25,16 @@ const ITEMS_TO_SHOW = 8
const wrapArrayIndex = (index: number, length: number) => const wrapArrayIndex = (index: number, length: number) =>
((index % length) + length) % length ((index % length) + length) % length
export const SpellingSuggestions: FC<{ type SpellingSuggestionsProps = {
word: Word word: Word
spellCheckLanguage?: string spellCheckLanguage?: string
spellChecker?: SpellChecker | null spellChecker?: SpellChecker | null
handleClose: () => void handleClose: () => void
handleLearnWord: () => void handleLearnWord: () => void
handleCorrectWord: (text: string) => void handleCorrectWord: (text: string) => void
}> = ({ }
export const SpellingSuggestions: FC<SpellingSuggestionsProps> = ({
word, word,
spellCheckLanguage, spellCheckLanguage,
spellChecker, spellChecker,
@ -37,8 +42,6 @@ export const SpellingSuggestions: FC<{
handleLearnWord, handleLearnWord,
handleCorrectWord, handleCorrectWord,
}) => { }) => {
const { t } = useTranslation()
const [suggestions, setSuggestions] = useState(() => const [suggestions, setSuggestions] = useState(() =>
Array.isArray(word.suggestions) Array.isArray(word.suggestions)
? word.suggestions.slice(0, ITEMS_TO_SHOW) ? word.suggestions.slice(0, ITEMS_TO_SHOW)
@ -47,10 +50,6 @@ export const SpellingSuggestions: FC<{
const [waiting, setWaiting] = useState(!word.suggestions) const [waiting, setWaiting] = useState(!word.suggestions)
const [selectedIndex, setSelectedIndex] = useState(0)
const itemsLength = suggestions.length + 1
useEffect(() => { useEffect(() => {
if (!word.suggestions) { if (!word.suggestions) {
spellChecker spellChecker
@ -85,6 +84,46 @@ export const SpellingSuggestions: FC<{
return null return null
} }
const innerProps = {
suggestions,
waiting,
handleClose,
handleCorrectWord,
handleLearnWord,
language,
}
return (
<BootstrapVersionSwitcher
bs3={<B3SpellingSuggestions {...innerProps} />}
bs5={<B5SpellingSuggestions {...innerProps} />}
/>
)
}
type SpellingSuggestionsInnerProps = {
suggestions: string[]
waiting: boolean
handleClose: () => void
handleCorrectWord: (text: string) => void
handleLearnWord: () => void
language: SpellCheckLanguage
}
const B3SpellingSuggestions: FC<SpellingSuggestionsInnerProps> = ({
suggestions,
waiting,
language,
handleClose,
handleCorrectWord,
handleLearnWord,
}) => {
const { t } = useTranslation()
const [selectedIndex, setSelectedIndex] = useState(0)
const itemsLength = suggestions.length + 1
return ( return (
<ul <ul
className={classnames('dropdown-menu', 'dropdown-menu-unpositioned', { className={classnames('dropdown-menu', 'dropdown-menu-unpositioned', {
@ -113,7 +152,7 @@ export const SpellingSuggestions: FC<{
{Array.isArray(suggestions) && ( {Array.isArray(suggestions) && (
<> <>
{suggestions.map((suggestion, index) => ( {suggestions.map((suggestion, index) => (
<ListItem <BS3ListItem
key={suggestion} key={suggestion}
content={suggestion} content={suggestion}
selected={index === selectedIndex} selected={index === selectedIndex}
@ -126,7 +165,7 @@ export const SpellingSuggestions: FC<{
{suggestions.length > 0 && <li className="divider" />} {suggestions.length > 0 && <li className="divider" />}
</> </>
)} )}
<ListItem <BS3ListItem
content={t('add_to_dictionary')} content={t('add_to_dictionary')}
selected={selectedIndex === itemsLength - 1} selected={selectedIndex === itemsLength - 1}
handleClick={event => { handleClick={event => {
@ -155,7 +194,7 @@ export const SpellingSuggestions: FC<{
) )
} }
const ListItem: FC<{ const BS3ListItem: FC<{
content: string content: string
selected: boolean selected: boolean
handleClick: MouseEventHandler<HTMLButtonElement> handleClick: MouseEventHandler<HTMLButtonElement>
@ -183,3 +222,94 @@ const ListItem: FC<{
</li> </li>
) )
} }
const B5SpellingSuggestions: FC<SpellingSuggestionsInnerProps> = ({
suggestions,
waiting,
language,
handleClose,
handleCorrectWord,
handleLearnWord,
}) => {
const { t } = useTranslation()
return (
<Dropdown>
<Dropdown.Menu
className={classnames('dropdown-menu', 'dropdown-menu-unpositioned', {
hidden: waiting,
})}
show={!waiting}
tabIndex={0}
role="menu"
onKeyDown={event => {
switch (event.code) {
case 'Escape':
case 'Tab':
event.preventDefault()
handleClose()
break
}
}}
>
{Array.isArray(suggestions) &&
suggestions.map((suggestion, index) => (
<BS5ListItem
key={suggestion}
content={suggestion}
handleClick={event => {
event.preventDefault()
handleCorrectWord(suggestion)
}}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={index === 0}
/>
))}
{suggestions?.length > 0 && <Dropdown.Divider />}
<BS5ListItem
content={t('add_to_dictionary')}
handleClick={event => {
event.preventDefault()
handleLearnWord()
}}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={suggestions?.length === 0}
/>
<Dropdown.Divider />
<SpellingSuggestionsLanguage
language={language}
handleClose={handleClose}
/>
{getMeta('ol-isSaas') && (
<>
<Dropdown.Divider />
<SpellingSuggestionsFeedback />
</>
)}
</Dropdown.Menu>
</Dropdown>
)
}
const BS5ListItem: FC<{
content: string
handleClick: MouseEventHandler<HTMLButtonElement>
autoFocus?: boolean
}> = ({ content, handleClick, autoFocus }) => {
const handleListItem = useCallback(
(node: HTMLElement | null) => {
if (node && autoFocus) node.focus()
},
[autoFocus]
)
return (
<Dropdown.Item
role="menuitem"
className="btn-link text-left dropdown-menu-button"
onClick={handleClick}
ref={handleListItem}
>
{content}
</Dropdown.Item>
)
}

View file

@ -9,6 +9,15 @@
var(--spacing-04); var(--spacing-04);
} }
.dropdown-menu.dropdown-menu-unpositioned {
position: unset;
top: unset;
left: unset;
z-index: unset;
display: block;
float: unset;
}
.dropdown-menu { .dropdown-menu {
@include shadow-md; @include shadow-md;