mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[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:
parent
d4fa37b75d
commit
053831b48c
5 changed files with 200 additions and 39 deletions
|
@ -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',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue