Add keyboard shortcuts for Layout actions (#30494)
GitOrigin-RevId: 7978a579e719c4cd593c02662406e364b19f87e0
This commit is contained in:
+3
-33
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, forwardRef } from 'react'
|
||||
import { memo, forwardRef } from 'react'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
useLayoutContext,
|
||||
} from '../../../shared/context/layout-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import useEventListener from '../../../shared/hooks/use-event-listener'
|
||||
import { DetachRole } from '@/shared/context/detach-context'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
@@ -96,43 +95,14 @@ function BS5DetachDisabled() {
|
||||
|
||||
function LayoutDropdownButton() {
|
||||
const {
|
||||
reattach,
|
||||
detach,
|
||||
detachIsLinked,
|
||||
detachRole,
|
||||
changeLayout,
|
||||
view,
|
||||
pdfLayout,
|
||||
handleChangeLayout,
|
||||
handleDetach,
|
||||
} = useLayoutContext()
|
||||
|
||||
const handleDetach = useCallback(() => {
|
||||
detach()
|
||||
eventTracking.sendMB('project-layout-detach')
|
||||
}, [detach])
|
||||
|
||||
const handleReattach = useCallback(() => {
|
||||
if (detachRole !== 'detacher') {
|
||||
return
|
||||
}
|
||||
reattach()
|
||||
eventTracking.sendMB('project-layout-reattach')
|
||||
}, [detachRole, reattach])
|
||||
|
||||
// reattach when the PDF pane opens
|
||||
useEventListener('ui:pdf-open', handleReattach)
|
||||
|
||||
const handleChangeLayout = useCallback(
|
||||
(newLayout: IdeLayout, newView?: IdeView) => {
|
||||
handleReattach()
|
||||
changeLayout(newLayout, newView)
|
||||
eventTracking.sendMB('project-layout-change', {
|
||||
layout: newLayout,
|
||||
view: newView,
|
||||
})
|
||||
},
|
||||
[changeLayout, handleReattach]
|
||||
)
|
||||
|
||||
return (
|
||||
<LayoutDropdownButtonUi
|
||||
processing={!detachIsLinked && detachRole === 'detacher'}
|
||||
|
||||
+39
-40
@@ -7,12 +7,13 @@ import {
|
||||
IdeView,
|
||||
useLayoutContext,
|
||||
} from '@/shared/context/layout-context'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { DetachRole } from '@/shared/context/detach-context'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
import { Shortcut } from '@/shared/components/shortcut'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type LayoutOption = 'sideBySide' | 'editorOnly' | 'pdfOnly' | 'detachedPdf'
|
||||
|
||||
@@ -53,77 +54,63 @@ const LayoutDropdownItem = ({
|
||||
disabled = false,
|
||||
processing = false,
|
||||
leadingIcon,
|
||||
trailingIcon,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean
|
||||
leadingIcon: string
|
||||
leadingIcon: React.ReactNode
|
||||
trailingIcon?: React.ReactNode
|
||||
onClick: () => void
|
||||
children: React.ReactNode
|
||||
processing?: boolean
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
let trailingIcon: string | React.ReactNode | null = null
|
||||
if (processing) {
|
||||
trailingIcon = <OLSpinner size="sm" />
|
||||
leadingIcon = <OLSpinner size="sm" />
|
||||
} else if (active) {
|
||||
trailingIcon = 'check'
|
||||
leadingIcon = 'check'
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
active={active}
|
||||
aria-current={active}
|
||||
trailingIcon={trailingIcon}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
leadingIcon={leadingIcon}
|
||||
trailingIcon={trailingIcon}
|
||||
className={classNames({ 'dropdown-item-wide': isMac })}
|
||||
>
|
||||
{children}
|
||||
</DropdownItem>
|
||||
)
|
||||
}
|
||||
|
||||
const shortcuts: Record<LayoutOption, string[] | null> = isMac
|
||||
? {
|
||||
editorOnly: ['⌃', '⌘', '←'],
|
||||
pdfOnly: ['⌃', '⌘', '→'],
|
||||
sideBySide: ['⌃', '⌘', '↓'],
|
||||
detachedPdf: ['⌃', '⌘', '↑'],
|
||||
}
|
||||
: {
|
||||
editorOnly: null,
|
||||
pdfOnly: null,
|
||||
sideBySide: null,
|
||||
detachedPdf: null,
|
||||
}
|
||||
|
||||
export default function ChangeLayoutOptions() {
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
const {
|
||||
reattach,
|
||||
detach,
|
||||
detachIsLinked,
|
||||
detachRole,
|
||||
changeLayout,
|
||||
view,
|
||||
pdfLayout,
|
||||
handleChangeLayout,
|
||||
handleDetach,
|
||||
} = useLayoutContext()
|
||||
|
||||
const handleDetach = useCallback(() => {
|
||||
detach()
|
||||
sendEvent('project-layout-detach')
|
||||
}, [detach, sendEvent])
|
||||
|
||||
const handleReattach = useCallback(() => {
|
||||
if (detachRole !== 'detacher') {
|
||||
return
|
||||
}
|
||||
reattach()
|
||||
sendEvent('project-layout-reattach')
|
||||
}, [detachRole, reattach, sendEvent])
|
||||
|
||||
// reattach when the PDF pane opens
|
||||
useEventListener('ui:pdf-open', handleReattach)
|
||||
|
||||
const handleChangeLayout = useCallback(
|
||||
(newLayout: IdeLayout, newView?: IdeView) => {
|
||||
handleReattach()
|
||||
changeLayout(newLayout, newView)
|
||||
sendEvent('project-layout-change', {
|
||||
layout: newLayout,
|
||||
view: newView,
|
||||
})
|
||||
},
|
||||
[changeLayout, handleReattach, sendEvent]
|
||||
)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const detachable = 'BroadcastChannel' in window
|
||||
@@ -143,6 +130,9 @@ export default function ChangeLayoutOptions() {
|
||||
onClick={() => handleChangeLayout('sideBySide')}
|
||||
active={activeLayoutOption === 'sideBySide'}
|
||||
leadingIcon="splitscreen_right"
|
||||
trailingIcon={
|
||||
shortcuts.sideBySide && <Shortcut keys={shortcuts.sideBySide} />
|
||||
}
|
||||
>
|
||||
{t('split_view')}
|
||||
</LayoutDropdownItem>
|
||||
@@ -150,6 +140,9 @@ export default function ChangeLayoutOptions() {
|
||||
onClick={() => handleChangeLayout('flat', 'editor')}
|
||||
active={activeLayoutOption === 'editorOnly'}
|
||||
leadingIcon="edit"
|
||||
trailingIcon={
|
||||
shortcuts.editorOnly && <Shortcut keys={shortcuts.editorOnly} />
|
||||
}
|
||||
>
|
||||
{t('editor_only')}
|
||||
</LayoutDropdownItem>
|
||||
@@ -157,6 +150,9 @@ export default function ChangeLayoutOptions() {
|
||||
onClick={() => handleChangeLayout('flat', 'pdf')}
|
||||
active={activeLayoutOption === 'pdfOnly'}
|
||||
leadingIcon="picture_as_pdf"
|
||||
trailingIcon={
|
||||
shortcuts.pdfOnly && <Shortcut keys={shortcuts.pdfOnly} />
|
||||
}
|
||||
>
|
||||
{t('pdf_only')}
|
||||
</LayoutDropdownItem>
|
||||
@@ -165,6 +161,9 @@ export default function ChangeLayoutOptions() {
|
||||
active={activeLayoutOption === 'detachedPdf' && detachIsLinked}
|
||||
disabled={!detachable}
|
||||
leadingIcon="open_in_new"
|
||||
trailingIcon={
|
||||
shortcuts.detachedPdf && <Shortcut keys={shortcuts.detachedPdf} />
|
||||
}
|
||||
processing={waitingForDetachedLink}
|
||||
>
|
||||
{t('open_pdf_in_separate_tab')}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from 'classnames'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownDivider,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import FormControl from '@/shared/components/form/form-control'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
import { Shortcut } from '@/shared/components/shortcut'
|
||||
|
||||
const shortcuts = isMac
|
||||
? {
|
||||
@@ -171,21 +171,4 @@ function PdfZoomDropdown({
|
||||
)
|
||||
}
|
||||
|
||||
function Shortcut({ keys }: { keys: string[] }) {
|
||||
return (
|
||||
<span className="float-end">
|
||||
{keys.map((key, idx) => (
|
||||
<span
|
||||
className={classNames({
|
||||
'pdfjs-zoom-dropdown-mac-shortcut-char': key.length === 1,
|
||||
})}
|
||||
key={`${key}${idx}`}
|
||||
>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default PdfZoomDropdown
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
export function Shortcut({ keys }: { keys: string[] }) {
|
||||
return (
|
||||
<span>
|
||||
{keys.map((key, idx) => (
|
||||
<span
|
||||
className={classNames({
|
||||
'dropdown-shortcut-char': key.length === 1,
|
||||
})}
|
||||
key={`${key}${idx}`}
|
||||
>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { sendSearchEvent } from '@/features/event-tracking/search-events'
|
||||
import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
|
||||
import usePersistedState from '@/shared/hooks/use-persisted-state'
|
||||
import { repositionAllTooltips } from '@/features/source-editor/extensions/tooltips-reposition'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
|
||||
export type IdeLayout = 'sideBySide' | 'flat'
|
||||
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
|
||||
@@ -63,6 +64,8 @@ export type LayoutContextValue = LayoutContextOwnStates & {
|
||||
setProjectSearchIsOpen: Dispatch<SetStateAction<boolean>>
|
||||
setOpenFile: Dispatch<SetStateAction<BinaryFile | null>>
|
||||
restoreView: () => void
|
||||
handleChangeLayout: (newLayout: IdeLayout, newView?: IdeView) => void
|
||||
handleDetach: () => void
|
||||
}
|
||||
|
||||
const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
||||
@@ -89,6 +92,7 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const [prevRailIsOpen, setPrevRailIsOpen] = useState(railIsOpen)
|
||||
// Whether we came from a file or a document when we left the ide
|
||||
const lastIdeView = useRef<IdeView>('editor')
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
|
||||
const setView = useCallback(
|
||||
(value: IdeView | null) => {
|
||||
@@ -258,6 +262,68 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
changeLayout,
|
||||
])
|
||||
|
||||
const handleDetach = useCallback(() => {
|
||||
detach()
|
||||
sendEvent('project-layout-detach')
|
||||
}, [detach, sendEvent])
|
||||
|
||||
const handleReattach = useCallback(() => {
|
||||
if (detachRole !== 'detacher') {
|
||||
return
|
||||
}
|
||||
reattach()
|
||||
sendEvent('project-layout-reattach')
|
||||
}, [detachRole, reattach, sendEvent])
|
||||
|
||||
const handleChangeLayout = useCallback(
|
||||
(newLayout: IdeLayout, newView?: IdeView) => {
|
||||
handleReattach()
|
||||
changeLayout(newLayout, newView)
|
||||
sendEvent('project-layout-change', {
|
||||
layout: newLayout,
|
||||
view: newView,
|
||||
})
|
||||
},
|
||||
[changeLayout, handleReattach, sendEvent]
|
||||
)
|
||||
|
||||
useEventListener(
|
||||
'keydown',
|
||||
useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (
|
||||
isMac &&
|
||||
event.metaKey &&
|
||||
event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey
|
||||
) {
|
||||
switch (event.code) {
|
||||
case 'ArrowLeft': // Editor only
|
||||
event.preventDefault()
|
||||
handleChangeLayout('flat', 'editor')
|
||||
break
|
||||
case 'ArrowRight': // PDF only
|
||||
event.preventDefault()
|
||||
handleChangeLayout('flat', 'pdf')
|
||||
break
|
||||
case 'ArrowDown': // Split view
|
||||
event.preventDefault()
|
||||
handleChangeLayout('sideBySide')
|
||||
break
|
||||
case 'ArrowUp': // Open PDF in separate tab (detach)
|
||||
event.preventDefault()
|
||||
if ('BroadcastChannel' in window && detachRole !== 'detacher') {
|
||||
handleDetach()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
[detachRole, handleChangeLayout, handleDetach]
|
||||
)
|
||||
)
|
||||
|
||||
const value = useMemo<LayoutContextValue>(
|
||||
() => ({
|
||||
reattach,
|
||||
@@ -285,6 +351,8 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
setView,
|
||||
view,
|
||||
restoreView,
|
||||
handleChangeLayout,
|
||||
handleDetach,
|
||||
}),
|
||||
[
|
||||
reattach,
|
||||
@@ -312,6 +380,8 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
setView,
|
||||
view,
|
||||
restoreView,
|
||||
handleChangeLayout,
|
||||
handleDetach,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -272,3 +272,13 @@ $dropdown-item-min-height: 36px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dropdown-shortcut-char {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown-item-wide {
|
||||
min-width: 264px;
|
||||
}
|
||||
|
||||
@@ -385,12 +385,6 @@
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.pdfjs-zoom-dropdown-mac-shortcut-char {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pdfjs-custom-zoom-menu-item {
|
||||
display: block;
|
||||
pointer-events: initial !important;
|
||||
|
||||
+2
@@ -96,6 +96,8 @@ const createInitialValue = () =>
|
||||
openFile: null,
|
||||
setOpenFile: cy.stub(),
|
||||
restoreView: cy.stub(),
|
||||
handleChangeLayout: cy.stub(),
|
||||
handleDetach: cy.stub(),
|
||||
}) satisfies LayoutContextValue
|
||||
|
||||
const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import { UserId } from '../../../types/user'
|
||||
import { ProjectCompiler } from '../../../types/project-settings'
|
||||
import { ReferencesContext } from '@/features/ide-react/context/references-context'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
|
||||
// these constants can be imported in tests instead of
|
||||
// using magic strings
|
||||
@@ -429,6 +430,8 @@ const makeLayoutProvider = (
|
||||
layout.loadingStyleSheet
|
||||
)
|
||||
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
|
||||
useEventListener(
|
||||
'ui.toggle-review-panel',
|
||||
useCallback(() => {
|
||||
@@ -453,8 +456,34 @@ const makeLayoutProvider = (
|
||||
isLinked: detachIsLinked,
|
||||
role: detachRole,
|
||||
} = useDetachLayout()
|
||||
|
||||
const handleDetach = useCallback(() => {
|
||||
detach()
|
||||
sendEvent('project-layout-detach')
|
||||
}, [detach, sendEvent])
|
||||
|
||||
const handleReattach = useCallback(() => {
|
||||
if (detachRole !== 'detacher') {
|
||||
return
|
||||
}
|
||||
reattach()
|
||||
sendEvent('project-layout-reattach')
|
||||
}, [detachRole, reattach, sendEvent])
|
||||
|
||||
const handleChangeLayout = useCallback(
|
||||
(newLayout: IdeLayout, newView?: IdeView) => {
|
||||
handleReattach()
|
||||
changeLayout(newLayout, newView)
|
||||
sendEvent('project-layout-change', {
|
||||
layout: newLayout,
|
||||
view: newView,
|
||||
})
|
||||
},
|
||||
[changeLayout, handleReattach, sendEvent]
|
||||
)
|
||||
const pdfPreviewOpen =
|
||||
pdfLayout === 'sideBySide' || view === 'pdf' || detachRole === 'detacher'
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
reattach,
|
||||
@@ -482,6 +511,8 @@ const makeLayoutProvider = (
|
||||
setView,
|
||||
view,
|
||||
restoreView,
|
||||
handleChangeLayout,
|
||||
handleDetach,
|
||||
}),
|
||||
[
|
||||
reattach,
|
||||
@@ -509,6 +540,8 @@ const makeLayoutProvider = (
|
||||
setView,
|
||||
view,
|
||||
restoreView,
|
||||
handleChangeLayout,
|
||||
handleDetach,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user