Display current spell check language and option to change it (#21138)

GitOrigin-RevId: 87cf140a7e3e719125eb6d2df23d6c6bd6631fe8
This commit is contained in:
Alf Eaton 2024-10-17 14:52:58 +01:00 committed by Copybot
parent b5015b82c2
commit 1b2f5af1c0
14 changed files with 161 additions and 15 deletions

View file

@ -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": "",

View file

@ -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
}

View file

@ -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>
} }
/> />
) )

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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'

View file

@ -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>
</> </>

View file

@ -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')

View file

@ -163,7 +163,8 @@
border-bottom: 0; border-bottom: 0;
} }
&:hover { &:hover,
&:focus-within {
background-color: @link-color; background-color: @link-color;
label { label {

View file

@ -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 {

View file

@ -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;

View file

@ -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",

View file

@ -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>