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': {
|
||||
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 classnames from 'classnames'
|
||||
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 { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLTooltip
|
||||
id="spell-check-client-tooltip"
|
||||
|
@ -21,27 +23,31 @@ const SpellingSuggestionsFeedback: FC = () => {
|
|||
tooltipProps={{ className: 'split-test-badge-tooltip' }}
|
||||
overlayProps={{ placement: 'bottom', delay: 100 }}
|
||||
>
|
||||
<a
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSdD1wa5SiCZ7x_UF6e8vywTN82kSm6ou2rTKz-XBiEjNilOXQ/viewform"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<span
|
||||
className={classnames('badge', 'info-badge')}
|
||||
style={{ width: 14, height: 14 }}
|
||||
/>
|
||||
}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="info"
|
||||
className={classnames('align-middle', 'info-badge')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<span className="mx-2">{t('give_feedback')}</span>
|
||||
</a>
|
||||
<span>
|
||||
<PolymorphicComponent
|
||||
as={isBootstrap5() ? Dropdown.Item : 'a'}
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSdD1wa5SiCZ7x_UF6e8vywTN82kSm6ou2rTKz-XBiEjNilOXQ/viewform"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={bsVersion({ bs3: 'dropdown-menu-button' })}
|
||||
>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<span
|
||||
className={classnames('badge', 'info-badge')}
|
||||
style={{ width: 14, height: 14 }}
|
||||
/>
|
||||
}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="info"
|
||||
className={classnames('align-middle', 'info-badge')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<span className="mx-2">{t('give_feedback')}</span>
|
||||
</PolymorphicComponent>
|
||||
</span>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,11 @@ import { memo, useCallback } from 'react'
|
|||
import Icon from '@/shared/components/icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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<{
|
||||
language: { name: string }
|
||||
|
@ -27,12 +32,22 @@ export const SpellingSuggestionsLanguage = memo<{
|
|||
description={t('change_language')}
|
||||
overlayProps={{ placement: 'right', delay: 100 }}
|
||||
>
|
||||
<button
|
||||
className="btn-link text-left dropdown-menu-button"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icon type="cog" /> <span className="mx-1">{language.name}</span>
|
||||
</button>
|
||||
<span>
|
||||
<PolymorphicComponent
|
||||
as={isBootstrap5() ? Dropdown.Item : 'a'}
|
||||
className={bsVersion({
|
||||
bs3: 'btn-link text-left dropdown-menu-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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -15,6 +15,9 @@ import SpellingSuggestionsFeedback from './spelling-suggestions-feedback'
|
|||
import { SpellingSuggestionsLanguage } from './spelling-suggestions-language'
|
||||
import { captureException } from '@/infrastructure/error-reporter'
|
||||
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
|
||||
|
||||
|
@ -22,14 +25,16 @@ const ITEMS_TO_SHOW = 8
|
|||
const wrapArrayIndex = (index: number, length: number) =>
|
||||
((index % length) + length) % length
|
||||
|
||||
export const SpellingSuggestions: FC<{
|
||||
type SpellingSuggestionsProps = {
|
||||
word: Word
|
||||
spellCheckLanguage?: string
|
||||
spellChecker?: SpellChecker | null
|
||||
handleClose: () => void
|
||||
handleLearnWord: () => void
|
||||
handleCorrectWord: (text: string) => void
|
||||
}> = ({
|
||||
}
|
||||
|
||||
export const SpellingSuggestions: FC<SpellingSuggestionsProps> = ({
|
||||
word,
|
||||
spellCheckLanguage,
|
||||
spellChecker,
|
||||
|
@ -37,8 +42,6 @@ export const SpellingSuggestions: FC<{
|
|||
handleLearnWord,
|
||||
handleCorrectWord,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [suggestions, setSuggestions] = useState(() =>
|
||||
Array.isArray(word.suggestions)
|
||||
? word.suggestions.slice(0, ITEMS_TO_SHOW)
|
||||
|
@ -47,10 +50,6 @@ export const SpellingSuggestions: FC<{
|
|||
|
||||
const [waiting, setWaiting] = useState(!word.suggestions)
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
const itemsLength = suggestions.length + 1
|
||||
|
||||
useEffect(() => {
|
||||
if (!word.suggestions) {
|
||||
spellChecker
|
||||
|
@ -85,6 +84,46 @@ export const SpellingSuggestions: FC<{
|
|||
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 (
|
||||
<ul
|
||||
className={classnames('dropdown-menu', 'dropdown-menu-unpositioned', {
|
||||
|
@ -113,7 +152,7 @@ export const SpellingSuggestions: FC<{
|
|||
{Array.isArray(suggestions) && (
|
||||
<>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<ListItem
|
||||
<BS3ListItem
|
||||
key={suggestion}
|
||||
content={suggestion}
|
||||
selected={index === selectedIndex}
|
||||
|
@ -126,7 +165,7 @@ export const SpellingSuggestions: FC<{
|
|||
{suggestions.length > 0 && <li className="divider" />}
|
||||
</>
|
||||
)}
|
||||
<ListItem
|
||||
<BS3ListItem
|
||||
content={t('add_to_dictionary')}
|
||||
selected={selectedIndex === itemsLength - 1}
|
||||
handleClick={event => {
|
||||
|
@ -155,7 +194,7 @@ export const SpellingSuggestions: FC<{
|
|||
)
|
||||
}
|
||||
|
||||
const ListItem: FC<{
|
||||
const BS3ListItem: FC<{
|
||||
content: string
|
||||
selected: boolean
|
||||
handleClick: MouseEventHandler<HTMLButtonElement>
|
||||
|
@ -183,3 +222,94 @@ const ListItem: FC<{
|
|||
</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);
|
||||
}
|
||||
|
||||
.dropdown-menu.dropdown-menu-unpositioned {
|
||||
position: unset;
|
||||
top: unset;
|
||||
left: unset;
|
||||
z-index: unset;
|
||||
display: block;
|
||||
float: unset;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@include shadow-md;
|
||||
|
||||
|
|
Loading…
Reference in a new issue