mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-24 21:12:38 -04:00
Display current spell check language and option to change it (#21138)
GitOrigin-RevId: 87cf140a7e3e719125eb6d2df23d6c6bd6631fe8
This commit is contained in:
parent
b5015b82c2
commit
1b2f5af1c0
14 changed files with 161 additions and 15 deletions
|
@ -183,6 +183,7 @@
|
|||
"center": "",
|
||||
"change": "",
|
||||
"change_currency": "",
|
||||
"change_language": "",
|
||||
"change_or_cancel-cancel": "",
|
||||
"change_or_cancel-change": "",
|
||||
"change_or_cancel-or": "",
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { createContext, FC, useCallback, useContext, useState } from 'react'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
|
||||
type EditorLeftMenuState = {
|
||||
settingToFocus?: string
|
||||
}
|
||||
|
||||
export const EditorLeftMenuContext = createContext<
|
||||
EditorLeftMenuState | undefined
|
||||
>(undefined)
|
||||
|
||||
export const EditorLeftMenuProvider: FC = ({ children }) => {
|
||||
const [value, setValue] = useState<EditorLeftMenuState>(() => ({
|
||||
settingToFocus: undefined,
|
||||
}))
|
||||
|
||||
useEventListener(
|
||||
'ui.focus-setting',
|
||||
useCallback(event => {
|
||||
setValue(value => ({
|
||||
...value,
|
||||
settingToFocus: (event as CustomEvent<string>).detail,
|
||||
}))
|
||||
}, [])
|
||||
)
|
||||
|
||||
return (
|
||||
<EditorLeftMenuContext.Provider value={value}>
|
||||
{children}
|
||||
</EditorLeftMenuContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useEditorLeftMenuContext = () => {
|
||||
const value = useContext(EditorLeftMenuContext)
|
||||
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
`useEditorLeftMenuContext is only available inside EditorLeftMenuProvider`
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
|
@ -7,6 +7,8 @@ import { lazy, memo, Suspense } from 'react'
|
|||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { Offcanvas } from 'react-bootstrap-5'
|
||||
import { EditorLeftMenuProvider } from './editor-left-menu-context'
|
||||
|
||||
const EditorLeftMenuBody = lazy(() => import('./editor-left-menu-body'))
|
||||
|
||||
function EditorLeftMenu() {
|
||||
|
@ -19,7 +21,7 @@ function EditorLeftMenu() {
|
|||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<>
|
||||
<EditorLeftMenuProvider>
|
||||
<AccessibleModal
|
||||
backdropClassName="left-menu-modal-backdrop"
|
||||
keyboard
|
||||
|
@ -37,10 +39,10 @@ function EditorLeftMenu() {
|
|||
</Modal.Body>
|
||||
</AccessibleModal>
|
||||
{leftMenuShown && <LeftMenuMask />}
|
||||
</>
|
||||
</EditorLeftMenuProvider>
|
||||
}
|
||||
bs5={
|
||||
<>
|
||||
<EditorLeftMenuProvider>
|
||||
<Offcanvas
|
||||
show={leftMenuShown}
|
||||
onHide={closeLeftMenu}
|
||||
|
@ -59,7 +61,7 @@ function EditorLeftMenu() {
|
|||
</Offcanvas.Body>
|
||||
</Offcanvas>
|
||||
{leftMenuShown && <LeftMenuMask />}
|
||||
</>
|
||||
</EditorLeftMenuProvider>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -2,8 +2,9 @@ import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/boots
|
|||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
|
||||
import { ChangeEventHandler, useCallback } from 'react'
|
||||
import { ChangeEventHandler, useCallback, useEffect, useRef } from 'react'
|
||||
import { Spinner } from 'react-bootstrap-5'
|
||||
import { useEditorLeftMenuContext } from '@/features/editor-left-menu/components/editor-left-menu-context'
|
||||
|
||||
type PossibleValue = string | number | boolean
|
||||
|
||||
|
@ -54,6 +55,25 @@ export default function SettingsMenuSelect<T extends PossibleValue = string>({
|
|||
[onChange, value]
|
||||
)
|
||||
|
||||
const { settingToFocus } = useEditorLeftMenuContext()
|
||||
|
||||
const selectRef = useRef<HTMLSelectElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (settingToFocus === name && selectRef.current) {
|
||||
selectRef.current.scrollIntoView({
|
||||
block: 'center',
|
||||
behavior: 'smooth',
|
||||
})
|
||||
selectRef.current.focus()
|
||||
}
|
||||
|
||||
// clear the focus setting
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('ui.focus-setting', { detail: undefined })
|
||||
)
|
||||
}, [name, settingToFocus])
|
||||
|
||||
return (
|
||||
<OLFormGroup
|
||||
controlId={`settings-menu-${name}`}
|
||||
|
@ -84,6 +104,7 @@ export default function SettingsMenuSelect<T extends PossibleValue = string>({
|
|||
onChange={handleChange}
|
||||
value={value?.toString()}
|
||||
disabled={disabled}
|
||||
ref={selectRef}
|
||||
>
|
||||
{options.map(option => (
|
||||
<option
|
||||
|
|
|
@ -167,11 +167,13 @@ const createSpellingSuggestionList = (word: Word) => (view: EditorView) => {
|
|||
word={word}
|
||||
spellCheckLanguage={getSpellCheckLanguage(view.state)}
|
||||
spellChecker={getSpellChecker(view.state)}
|
||||
handleClose={() => {
|
||||
handleClose={(focus = true) => {
|
||||
view.dispatch({
|
||||
effects: hideSpellingMenu.of(null),
|
||||
})
|
||||
view.focus()
|
||||
if (focus) {
|
||||
view.focus()
|
||||
}
|
||||
}}
|
||||
handleLearnWord={() => {
|
||||
learnWordRequest(word)
|
||||
|
|
|
@ -39,7 +39,12 @@ const SpellingSuggestionsFeedback: FC = () => {
|
|||
rel="noopener noreferrer"
|
||||
>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<span className={classnames('badge', badgeClass)} />}
|
||||
bs3={
|
||||
<span
|
||||
className={classnames('badge', badgeClass)}
|
||||
style={{ width: 14, height: 14 }}
|
||||
/>
|
||||
}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="info"
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
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'
|
||||
|
||||
export const SpellingSuggestionsLanguage = memo<{
|
||||
language: { name: string }
|
||||
handleClose: (focus: boolean) => void
|
||||
}>(({ language, handleClose }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
// open the left menu
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('ui.toggle-left-menu', { detail: true })
|
||||
)
|
||||
// focus the spell check setting
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('ui.focus-setting', { detail: 'spellCheckLanguage' })
|
||||
)
|
||||
handleClose(false)
|
||||
}, [handleClose])
|
||||
|
||||
return (
|
||||
<OLTooltip
|
||||
id="spell-check-client-tooltip"
|
||||
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>
|
||||
</OLTooltip>
|
||||
)
|
||||
})
|
||||
SpellingSuggestionsLanguage.displayName = 'SpellingSuggestionsLanguage'
|
|
@ -13,6 +13,7 @@ import classnames from 'classnames'
|
|||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import SpellingSuggestionsFeedback from './spelling-suggestions-feedback'
|
||||
import { SpellingSuggestionsLanguage } from './spelling-suggestions-language'
|
||||
import { captureException } from '@/infrastructure/error-reporter'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
|
@ -83,6 +84,10 @@ export const SpellingSuggestions: FC<{
|
|||
|
||||
const spellCheckClientEnabled = useFeatureFlag('spell-check-client')
|
||||
|
||||
if (!language) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={classnames('dropdown-menu', 'dropdown-menu-unpositioned', {
|
||||
|
@ -132,10 +137,19 @@ export const SpellingSuggestions: FC<{
|
|||
handleLearnWord()
|
||||
}}
|
||||
/>
|
||||
{spellCheckClientEnabled && language?.dic && (
|
||||
|
||||
<li className="divider" />
|
||||
<li role="menuitem">
|
||||
<SpellingSuggestionsLanguage
|
||||
language={language}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
</li>
|
||||
|
||||
{spellCheckClientEnabled && language.dic && (
|
||||
<>
|
||||
<li className="divider" />
|
||||
<li>
|
||||
<li role="menuitem">
|
||||
<SpellingSuggestionsFeedback />
|
||||
</li>
|
||||
</>
|
||||
|
|
|
@ -16,6 +16,7 @@ import { DetachRole } from './detach-context'
|
|||
import { debugConsole } from '@/utils/debugging'
|
||||
import { BinaryFile } from '@/features/file-view/types/binary-file'
|
||||
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
|
||||
export type IdeLayout = 'sideBySide' | 'flat'
|
||||
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
|
||||
|
@ -106,6 +107,16 @@ export const LayoutProvider: FC = ({ children }) => {
|
|||
const [leftMenuShown, setLeftMenuShown] =
|
||||
useScopeValue<boolean>('ui.leftMenuShown')
|
||||
|
||||
useEventListener(
|
||||
'ui.toggle-left-menu',
|
||||
useCallback(
|
||||
event => {
|
||||
setLeftMenuShown((event as CustomEvent<boolean>).detail)
|
||||
},
|
||||
[setLeftMenuShown]
|
||||
)
|
||||
)
|
||||
|
||||
// whether to display the editor and preview side-by-side or full-width ("flat")
|
||||
const [pdfLayout, setPdfLayout] = useScopeValue<IdeLayout>('ui.pdfLayout')
|
||||
|
||||
|
|
|
@ -163,7 +163,8 @@
|
|||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: @link-color;
|
||||
|
||||
label {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
overflow: hidden auto;
|
||||
transition: left ease-in-out 0.5s;
|
||||
font-size: var(--font-size-02);
|
||||
width: 320px;
|
||||
width: 340px;
|
||||
|
||||
&.shown {
|
||||
left: 0;
|
||||
|
@ -147,7 +147,8 @@
|
|||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: var(--bg-info-01);
|
||||
|
||||
label {
|
||||
|
|
|
@ -781,7 +781,7 @@
|
|||
|
||||
@content-margin-vertical: @line-height-computed;
|
||||
|
||||
@left-menu-width: 320px;
|
||||
@left-menu-width: 340px;
|
||||
@left-menu-animation-duration: 0.35s;
|
||||
|
||||
@toolbar-border-color: @neutral-80;
|
||||
|
|
|
@ -249,6 +249,7 @@
|
|||
"certificate": "Certificate",
|
||||
"change": "Change",
|
||||
"change_currency": "Change currency",
|
||||
"change_language": "Change language",
|
||||
"change_or_cancel-cancel": "cancel",
|
||||
"change_or_cancel-change": "Change",
|
||||
"change_or_cancel-or": "or",
|
||||
|
|
|
@ -35,6 +35,7 @@ import { OnlineUsersProvider } from '@/features/ide-react/context/online-users-c
|
|||
import { PermissionsProvider } from '@/features/ide-react/context/permissions-context'
|
||||
import { ReferencesProvider } from '@/features/ide-react/context/references-context'
|
||||
import { SnapshotProvider } from '@/features/ide-react/context/snapshot-context'
|
||||
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
|
||||
|
||||
// these constants can be imported in tests instead of
|
||||
// using magic strings
|
||||
|
@ -159,6 +160,7 @@ export function EditorProviders({
|
|||
DetachCompileProvider,
|
||||
DetachProvider,
|
||||
EditorProvider,
|
||||
EditorLeftMenuProvider,
|
||||
EditorManagerProvider,
|
||||
SnapshotProvider,
|
||||
FileTreeDataProvider,
|
||||
|
@ -206,7 +208,9 @@ export function EditorProviders({
|
|||
<Providers.OnlineUsersProvider>
|
||||
<Providers.MetadataProvider>
|
||||
<Providers.OutlineProvider>
|
||||
{children}
|
||||
<Providers.EditorLeftMenuProvider>
|
||||
{children}
|
||||
</Providers.EditorLeftMenuProvider>
|
||||
</Providers.OutlineProvider>
|
||||
</Providers.MetadataProvider>
|
||||
</Providers.OnlineUsersProvider>
|
||||
|
|
Loading…
Reference in a new issue