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": "",
|
"center": "",
|
||||||
"change": "",
|
"change": "",
|
||||||
"change_currency": "",
|
"change_currency": "",
|
||||||
|
"change_language": "",
|
||||||
"change_or_cancel-cancel": "",
|
"change_or_cancel-cancel": "",
|
||||||
"change_or_cancel-change": "",
|
"change_or_cancel-change": "",
|
||||||
"change_or_cancel-or": "",
|
"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 { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||||
import { Offcanvas } from 'react-bootstrap-5'
|
import { Offcanvas } from 'react-bootstrap-5'
|
||||||
|
import { EditorLeftMenuProvider } from './editor-left-menu-context'
|
||||||
|
|
||||||
const EditorLeftMenuBody = lazy(() => import('./editor-left-menu-body'))
|
const EditorLeftMenuBody = lazy(() => import('./editor-left-menu-body'))
|
||||||
|
|
||||||
function EditorLeftMenu() {
|
function EditorLeftMenu() {
|
||||||
|
@ -19,7 +21,7 @@ function EditorLeftMenu() {
|
||||||
return (
|
return (
|
||||||
<BootstrapVersionSwitcher
|
<BootstrapVersionSwitcher
|
||||||
bs3={
|
bs3={
|
||||||
<>
|
<EditorLeftMenuProvider>
|
||||||
<AccessibleModal
|
<AccessibleModal
|
||||||
backdropClassName="left-menu-modal-backdrop"
|
backdropClassName="left-menu-modal-backdrop"
|
||||||
keyboard
|
keyboard
|
||||||
|
@ -37,10 +39,10 @@ function EditorLeftMenu() {
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</AccessibleModal>
|
</AccessibleModal>
|
||||||
{leftMenuShown && <LeftMenuMask />}
|
{leftMenuShown && <LeftMenuMask />}
|
||||||
</>
|
</EditorLeftMenuProvider>
|
||||||
}
|
}
|
||||||
bs5={
|
bs5={
|
||||||
<>
|
<EditorLeftMenuProvider>
|
||||||
<Offcanvas
|
<Offcanvas
|
||||||
show={leftMenuShown}
|
show={leftMenuShown}
|
||||||
onHide={closeLeftMenu}
|
onHide={closeLeftMenu}
|
||||||
|
@ -59,7 +61,7 @@ function EditorLeftMenu() {
|
||||||
</Offcanvas.Body>
|
</Offcanvas.Body>
|
||||||
</Offcanvas>
|
</Offcanvas>
|
||||||
{leftMenuShown && <LeftMenuMask />}
|
{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 OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||||
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
|
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 { Spinner } from 'react-bootstrap-5'
|
||||||
|
import { useEditorLeftMenuContext } from '@/features/editor-left-menu/components/editor-left-menu-context'
|
||||||
|
|
||||||
type PossibleValue = string | number | boolean
|
type PossibleValue = string | number | boolean
|
||||||
|
|
||||||
|
@ -54,6 +55,25 @@ export default function SettingsMenuSelect<T extends PossibleValue = string>({
|
||||||
[onChange, value]
|
[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 (
|
return (
|
||||||
<OLFormGroup
|
<OLFormGroup
|
||||||
controlId={`settings-menu-${name}`}
|
controlId={`settings-menu-${name}`}
|
||||||
|
@ -84,6 +104,7 @@ export default function SettingsMenuSelect<T extends PossibleValue = string>({
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={value?.toString()}
|
value={value?.toString()}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
ref={selectRef}
|
||||||
>
|
>
|
||||||
{options.map(option => (
|
{options.map(option => (
|
||||||
<option
|
<option
|
||||||
|
|
|
@ -167,11 +167,13 @@ const createSpellingSuggestionList = (word: Word) => (view: EditorView) => {
|
||||||
word={word}
|
word={word}
|
||||||
spellCheckLanguage={getSpellCheckLanguage(view.state)}
|
spellCheckLanguage={getSpellCheckLanguage(view.state)}
|
||||||
spellChecker={getSpellChecker(view.state)}
|
spellChecker={getSpellChecker(view.state)}
|
||||||
handleClose={() => {
|
handleClose={(focus = true) => {
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: hideSpellingMenu.of(null),
|
effects: hideSpellingMenu.of(null),
|
||||||
})
|
})
|
||||||
view.focus()
|
if (focus) {
|
||||||
|
view.focus()
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
handleLearnWord={() => {
|
handleLearnWord={() => {
|
||||||
learnWordRequest(word)
|
learnWordRequest(word)
|
||||||
|
|
|
@ -39,7 +39,12 @@ const SpellingSuggestionsFeedback: FC = () => {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<BootstrapVersionSwitcher
|
<BootstrapVersionSwitcher
|
||||||
bs3={<span className={classnames('badge', badgeClass)} />}
|
bs3={
|
||||||
|
<span
|
||||||
|
className={classnames('badge', badgeClass)}
|
||||||
|
style={{ width: 14, height: 14 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
bs5={
|
bs5={
|
||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
type="info"
|
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 { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||||
import { sendMB } from '@/infrastructure/event-tracking'
|
import { sendMB } from '@/infrastructure/event-tracking'
|
||||||
import SpellingSuggestionsFeedback from './spelling-suggestions-feedback'
|
import SpellingSuggestionsFeedback from './spelling-suggestions-feedback'
|
||||||
|
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'
|
||||||
|
|
||||||
|
@ -83,6 +84,10 @@ export const SpellingSuggestions: FC<{
|
||||||
|
|
||||||
const spellCheckClientEnabled = useFeatureFlag('spell-check-client')
|
const spellCheckClientEnabled = useFeatureFlag('spell-check-client')
|
||||||
|
|
||||||
|
if (!language) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
className={classnames('dropdown-menu', 'dropdown-menu-unpositioned', {
|
className={classnames('dropdown-menu', 'dropdown-menu-unpositioned', {
|
||||||
|
@ -132,10 +137,19 @@ export const SpellingSuggestions: FC<{
|
||||||
handleLearnWord()
|
handleLearnWord()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{spellCheckClientEnabled && language?.dic && (
|
|
||||||
|
<li className="divider" />
|
||||||
|
<li role="menuitem">
|
||||||
|
<SpellingSuggestionsLanguage
|
||||||
|
language={language}
|
||||||
|
handleClose={handleClose}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{spellCheckClientEnabled && language.dic && (
|
||||||
<>
|
<>
|
||||||
<li className="divider" />
|
<li className="divider" />
|
||||||
<li>
|
<li role="menuitem">
|
||||||
<SpellingSuggestionsFeedback />
|
<SpellingSuggestionsFeedback />
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { DetachRole } from './detach-context'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
import { BinaryFile } from '@/features/file-view/types/binary-file'
|
import { BinaryFile } from '@/features/file-view/types/binary-file'
|
||||||
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
|
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
|
||||||
|
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||||
|
|
||||||
export type IdeLayout = 'sideBySide' | 'flat'
|
export type IdeLayout = 'sideBySide' | 'flat'
|
||||||
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
|
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
|
||||||
|
@ -106,6 +107,16 @@ export const LayoutProvider: FC = ({ children }) => {
|
||||||
const [leftMenuShown, setLeftMenuShown] =
|
const [leftMenuShown, setLeftMenuShown] =
|
||||||
useScopeValue<boolean>('ui.leftMenuShown')
|
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")
|
// whether to display the editor and preview side-by-side or full-width ("flat")
|
||||||
const [pdfLayout, setPdfLayout] = useScopeValue<IdeLayout>('ui.pdfLayout')
|
const [pdfLayout, setPdfLayout] = useScopeValue<IdeLayout>('ui.pdfLayout')
|
||||||
|
|
||||||
|
|
|
@ -163,7 +163,8 @@
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
background-color: @link-color;
|
background-color: @link-color;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
transition: left ease-in-out 0.5s;
|
transition: left ease-in-out 0.5s;
|
||||||
font-size: var(--font-size-02);
|
font-size: var(--font-size-02);
|
||||||
width: 320px;
|
width: 340px;
|
||||||
|
|
||||||
&.shown {
|
&.shown {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -147,7 +147,8 @@
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
background-color: var(--bg-info-01);
|
background-color: var(--bg-info-01);
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
|
|
@ -781,7 +781,7 @@
|
||||||
|
|
||||||
@content-margin-vertical: @line-height-computed;
|
@content-margin-vertical: @line-height-computed;
|
||||||
|
|
||||||
@left-menu-width: 320px;
|
@left-menu-width: 340px;
|
||||||
@left-menu-animation-duration: 0.35s;
|
@left-menu-animation-duration: 0.35s;
|
||||||
|
|
||||||
@toolbar-border-color: @neutral-80;
|
@toolbar-border-color: @neutral-80;
|
||||||
|
|
|
@ -249,6 +249,7 @@
|
||||||
"certificate": "Certificate",
|
"certificate": "Certificate",
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
"change_currency": "Change currency",
|
"change_currency": "Change currency",
|
||||||
|
"change_language": "Change language",
|
||||||
"change_or_cancel-cancel": "cancel",
|
"change_or_cancel-cancel": "cancel",
|
||||||
"change_or_cancel-change": "Change",
|
"change_or_cancel-change": "Change",
|
||||||
"change_or_cancel-or": "or",
|
"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 { PermissionsProvider } from '@/features/ide-react/context/permissions-context'
|
||||||
import { ReferencesProvider } from '@/features/ide-react/context/references-context'
|
import { ReferencesProvider } from '@/features/ide-react/context/references-context'
|
||||||
import { SnapshotProvider } from '@/features/ide-react/context/snapshot-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
|
// these constants can be imported in tests instead of
|
||||||
// using magic strings
|
// using magic strings
|
||||||
|
@ -159,6 +160,7 @@ export function EditorProviders({
|
||||||
DetachCompileProvider,
|
DetachCompileProvider,
|
||||||
DetachProvider,
|
DetachProvider,
|
||||||
EditorProvider,
|
EditorProvider,
|
||||||
|
EditorLeftMenuProvider,
|
||||||
EditorManagerProvider,
|
EditorManagerProvider,
|
||||||
SnapshotProvider,
|
SnapshotProvider,
|
||||||
FileTreeDataProvider,
|
FileTreeDataProvider,
|
||||||
|
@ -206,7 +208,9 @@ export function EditorProviders({
|
||||||
<Providers.OnlineUsersProvider>
|
<Providers.OnlineUsersProvider>
|
||||||
<Providers.MetadataProvider>
|
<Providers.MetadataProvider>
|
||||||
<Providers.OutlineProvider>
|
<Providers.OutlineProvider>
|
||||||
{children}
|
<Providers.EditorLeftMenuProvider>
|
||||||
|
{children}
|
||||||
|
</Providers.EditorLeftMenuProvider>
|
||||||
</Providers.OutlineProvider>
|
</Providers.OutlineProvider>
|
||||||
</Providers.MetadataProvider>
|
</Providers.MetadataProvider>
|
||||||
</Providers.OnlineUsersProvider>
|
</Providers.OnlineUsersProvider>
|
||||||
|
|
Loading…
Reference in a new issue