瀏覽代碼

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

GitOrigin-RevId: 87cf140a7e3e719125eb6d2df23d6c6bd6631fe8
Alf Eaton 1 年之前
父節點
當前提交
1b2f5af1c0

+ 1 - 0
services/web/frontend/extracted-translations.json

@@ -183,6 +183,7 @@
   "center": "",
   "change": "",
   "change_currency": "",
+  "change_language": "",
   "change_or_cancel-cancel": "",
   "change_or_cancel-change": "",
   "change_or_cancel-or": "",

+ 44 - 0
services/web/frontend/js/features/editor-left-menu/components/editor-left-menu-context.tsx

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

+ 6 - 4
services/web/frontend/js/features/editor-left-menu/components/editor-left-menu.tsx

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

+ 22 - 1
services/web/frontend/js/features/editor-left-menu/components/settings/settings-menu-select.tsx

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

+ 4 - 2
services/web/frontend/js/features/source-editor/extensions/spelling/context-menu.tsx

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

+ 6 - 1
services/web/frontend/js/features/source-editor/extensions/spelling/spelling-suggestions-feedback.tsx

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

+ 39 - 0
services/web/frontend/js/features/source-editor/extensions/spelling/spelling-suggestions-language.tsx

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

+ 16 - 2
services/web/frontend/js/features/source-editor/extensions/spelling/spelling-suggestions.tsx

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

+ 11 - 0
services/web/frontend/js/shared/context/layout-context.tsx

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

+ 2 - 1
services/web/frontend/stylesheets/app/editor/left-menu.less

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

+ 3 - 2
services/web/frontend/stylesheets/bootstrap-5/pages/editor/left-menu.scss

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

+ 1 - 1
services/web/frontend/stylesheets/variables/all.less

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

+ 1 - 0
services/web/locales/en.json

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

+ 5 - 1
services/web/test/frontend/helpers/editor-providers.jsx

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