mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #8082 from overleaf/ae-layout-shortcut
Add keyboard shortcuts to Layout menu GitOrigin-RevId: f6ea369f26024280df401b0444ddccf38e19c005
This commit is contained in:
parent
430b7528b2
commit
549a8497fb
4 changed files with 313 additions and 160 deletions
|
@ -1,4 +1,4 @@
|
|||
import { useCallback } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dropdown, MenuItem } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
@ -11,6 +11,7 @@ import IconPdfOnly from './icon-pdf-only'
|
|||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import useEventListener from '../../../shared/hooks/use-event-listener'
|
||||
import Shortcut from './shortcut'
|
||||
|
||||
function IconPlaceholder() {
|
||||
return <Icon type="" fw />
|
||||
|
@ -47,27 +48,46 @@ function IconCheckmark({ iconFor, pdfLayout, view, detachRole }) {
|
|||
return <IconPlaceholder />
|
||||
}
|
||||
|
||||
function PdfDetachMenuItem({ handleDetach, children }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!('BroadcastChannel' in window)) {
|
||||
return (
|
||||
<Tooltip
|
||||
id="detach-disabled"
|
||||
description={t('your_browser_does_not_support_this_feature')}
|
||||
overlayProps={{ placement: 'left' }}
|
||||
>
|
||||
<MenuItem disabled>{children}</MenuItem>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return <MenuItem onSelect={handleDetach}>{children}</MenuItem>
|
||||
function LayoutMenuItem({ checkmark, icon, text, shortcut, ...props }) {
|
||||
return (
|
||||
<MenuItem {...props}>
|
||||
<div className="layout-menu-item">
|
||||
<div className="layout-menu-item-start">
|
||||
<div>{checkmark}</div>
|
||||
<div>{icon}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
<Shortcut shortcut={shortcut} />
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
LayoutMenuItem.propTypes = {
|
||||
checkmark: PropTypes.node.isRequired,
|
||||
icon: PropTypes.node.isRequired,
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
shortcut: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func,
|
||||
}
|
||||
|
||||
PdfDetachMenuItem.propTypes = {
|
||||
handleDetach: PropTypes.func.isRequired,
|
||||
children: PropTypes.arrayOf(PropTypes.node).isRequired,
|
||||
function DetachDisabled() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
id="detach-disabled"
|
||||
description={t('your_browser_does_not_support_this_feature')}
|
||||
overlayProps={{ placement: 'left' }}
|
||||
>
|
||||
<LayoutMenuItem
|
||||
disabled
|
||||
checkmark={<IconPlaceholder />}
|
||||
icon={<IconDetach />}
|
||||
text={t('pdf_in_separate_tab')}
|
||||
shortcut="Control+Option+ArrowUp"
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function LayoutDropdownButton() {
|
||||
|
@ -111,6 +131,31 @@ function LayoutDropdownButton() {
|
|||
[changeLayout, handleReattach]
|
||||
)
|
||||
|
||||
const keyMap = useMemo(() => {
|
||||
return {
|
||||
ArrowDown: () => handleChangeLayout('sideBySide', null),
|
||||
ArrowRight: () => handleChangeLayout('flat', 'pdf'),
|
||||
ArrowLeft: () => handleChangeLayout('flat', 'editor'),
|
||||
ArrowUp: () => handleDetach(),
|
||||
}
|
||||
}, [handleChangeLayout, handleDetach])
|
||||
|
||||
useEffect(() => {
|
||||
const listener = event => {
|
||||
if (event.ctrlKey && event.altKey && event.key in keyMap) {
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
keyMap[event.key]()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', listener, true)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', listener, true)
|
||||
}
|
||||
}, [keyMap])
|
||||
|
||||
const processing = !detachIsLinked && detachRole === 'detacher'
|
||||
|
||||
// bsStyle is required for Dropdown.Toggle, but we will override style
|
||||
|
@ -137,76 +182,94 @@ function LayoutDropdownButton() {
|
|||
<span className="beta-badge" />
|
||||
</Tooltip>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu id="layout-dropdown-list">
|
||||
<MenuItem onSelect={() => handleChangeLayout('sideBySide')}>
|
||||
<IconCheckmark
|
||||
iconFor="sideBySide"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
detachRole={detachRole}
|
||||
/>
|
||||
<Icon type="columns" fw />
|
||||
{t('editor_and_pdf')}
|
||||
</MenuItem>
|
||||
<Dropdown.Menu className="layout-dropdown-list">
|
||||
<LayoutMenuItem
|
||||
onSelect={() => handleChangeLayout('sideBySide')}
|
||||
checkmark={
|
||||
<IconCheckmark
|
||||
iconFor="sideBySide"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
detachRole={detachRole}
|
||||
/>
|
||||
}
|
||||
icon={<Icon type="columns" fw />}
|
||||
text={t('editor_and_pdf')}
|
||||
shortcut="Control+Option+ArrowDown"
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
<LayoutMenuItem
|
||||
onSelect={() => handleChangeLayout('flat', 'editor')}
|
||||
className="menu-item-with-svg"
|
||||
>
|
||||
<IconCheckmark
|
||||
iconFor="editorOnly"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
detachRole={detachRole}
|
||||
/>
|
||||
<i className="fa fa-fw">
|
||||
<IconEditorOnly />
|
||||
</i>
|
||||
<span>
|
||||
checkmark={
|
||||
<IconCheckmark
|
||||
iconFor="editorOnly"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
detachRole={detachRole}
|
||||
/>
|
||||
}
|
||||
icon={
|
||||
<i className="fa fa-fw">
|
||||
<IconEditorOnly />
|
||||
</i>
|
||||
}
|
||||
text={
|
||||
<Trans
|
||||
i18nKey="editor_only_hide_pdf"
|
||||
components={[
|
||||
<span key="editor_only_hide_pdf" className="subdued" />,
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
</MenuItem>
|
||||
}
|
||||
shortcut="Control+Option+ArrowLeft"
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
<LayoutMenuItem
|
||||
onSelect={() => handleChangeLayout('flat', 'pdf')}
|
||||
className="menu-item-with-svg"
|
||||
>
|
||||
<IconCheckmark
|
||||
iconFor="pdfOnly"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
detachRole={detachRole}
|
||||
/>
|
||||
<i className="fa fa-fw">
|
||||
<IconPdfOnly />
|
||||
</i>
|
||||
<span>
|
||||
checkmark={
|
||||
<IconCheckmark
|
||||
iconFor="pdfOnly"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
detachRole={detachRole}
|
||||
/>
|
||||
}
|
||||
icon={
|
||||
<i className="fa fa-fw">
|
||||
<IconPdfOnly />
|
||||
</i>
|
||||
}
|
||||
text={
|
||||
<Trans
|
||||
i18nKey="pdf_only_hide_editor"
|
||||
components={[
|
||||
<span key="pdf_only_hide_editor" className="subdued" />,
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
</MenuItem>
|
||||
}
|
||||
shortcut="Control+Option+ArrowRight"
|
||||
/>
|
||||
|
||||
{detachRole === 'detacher' ? (
|
||||
<MenuItem>
|
||||
{detachIsLinked ? <IconChecked /> : <IconRefresh />}
|
||||
<IconDetach />
|
||||
{t('pdf_in_separate_tab')}
|
||||
</MenuItem>
|
||||
{'BroadcastChannel' in window ? (
|
||||
<LayoutMenuItem
|
||||
onSelect={() => handleDetach()}
|
||||
checkmark={
|
||||
detachRole === 'detacher' ? (
|
||||
detachIsLinked ? (
|
||||
<IconChecked />
|
||||
) : (
|
||||
<IconRefresh />
|
||||
)
|
||||
) : (
|
||||
<IconPlaceholder />
|
||||
)
|
||||
}
|
||||
icon={<IconDetach />}
|
||||
text={t('pdf_in_separate_tab')}
|
||||
shortcut="Control+Option+ArrowUp"
|
||||
/>
|
||||
) : (
|
||||
<PdfDetachMenuItem handleDetach={handleDetach}>
|
||||
<IconPlaceholder />
|
||||
<IconDetach />
|
||||
{t('pdf_in_separate_tab')}
|
||||
</PdfDetachMenuItem>
|
||||
<DetachDisabled />
|
||||
)}
|
||||
|
||||
<MenuItem divider />
|
||||
|
@ -232,7 +295,7 @@ function LayoutDropdownButton() {
|
|||
)
|
||||
}
|
||||
|
||||
export default LayoutDropdownButton
|
||||
export default memo(LayoutDropdownButton)
|
||||
|
||||
IconCheckmark.propTypes = {
|
||||
iconFor: PropTypes.string.isRequired,
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { Fragment, memo } from 'react'
|
||||
|
||||
const isMac = /Mac/.test(window.navigator.platform)
|
||||
|
||||
const symbols = isMac
|
||||
? {
|
||||
CommandOrControl: '⌘',
|
||||
Option: '⌥',
|
||||
Control: '⌃',
|
||||
Shift: '⇧',
|
||||
ArrowRight: '→',
|
||||
ArrowDown: '↓',
|
||||
ArrowLeft: '←',
|
||||
ArrowUp: '↑',
|
||||
}
|
||||
: {
|
||||
CommandOrControl: 'Ctrl',
|
||||
Control: 'Ctrl',
|
||||
Option: 'Alt',
|
||||
Shift: 'Shift',
|
||||
ArrowRight: '→',
|
||||
ArrowDown: '↓',
|
||||
ArrowLeft: '←',
|
||||
ArrowUp: '↑',
|
||||
}
|
||||
|
||||
const separator = isMac ? '' : '+'
|
||||
|
||||
const chooseCharacter = (input: string): string =>
|
||||
input in symbols ? symbols[input] : input
|
||||
|
||||
const Shortcut = ({ shortcut }: { shortcut: string }) => {
|
||||
return (
|
||||
<div className="shortcut" aria-hidden="true">
|
||||
{shortcut.split('+').map((item, index) => {
|
||||
const char = chooseCharacter(item)
|
||||
|
||||
return (
|
||||
<Fragment key={item}>
|
||||
{index > 0 && separator}
|
||||
{char.length === 1 ? (
|
||||
<span className="shortcut-symbol">{char}</span>
|
||||
) : (
|
||||
char
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Shortcut)
|
|
@ -161,30 +161,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
#layout-dropdown {
|
||||
// override style added by required bsStyle react-bootstrap prop
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.layout-dropdown {
|
||||
.pdf-detach-survey {
|
||||
display: flex;
|
||||
font-size: @font-size-small;
|
||||
}
|
||||
|
||||
.pdf-detach-survey-text {
|
||||
margin-left: @margin-sm;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
#layout-dropdown-list {
|
||||
a {
|
||||
i {
|
||||
margin-right: @margin-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-cobranding-logo {
|
||||
display: block;
|
||||
width: auto;
|
||||
|
@ -462,66 +438,3 @@
|
|||
.opacity(0.65);
|
||||
.box-shadow(none);
|
||||
}
|
||||
|
||||
.menu-item-with-svg {
|
||||
a {
|
||||
align-items: center;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-hover-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.subdued {
|
||||
color: @dropdown-link-disabled-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-disabled-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
.subdued {
|
||||
color: @dropdown-link-disabled-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-disabled-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,6 +120,9 @@ button.dropdown-toggle.dropdown-toggle-no-background {
|
|||
.subdued {
|
||||
color: @dropdown-link-hover-color;
|
||||
}
|
||||
div {
|
||||
color: @dropdown-link-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,3 +255,124 @@ button.dropdown-toggle.dropdown-toggle-no-background {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-dropdown {
|
||||
.dropdown-toggle {
|
||||
// override style added by required bsStyle react-bootstrap prop
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.dropdown-menu > li {
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
> a:hover,
|
||||
> a:focus {
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-hover-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.subdued {
|
||||
color: @dropdown-link-disabled-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-disabled-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
> a:hover,
|
||||
> a:focus {
|
||||
.subdued {
|
||||
color: @dropdown-link-disabled-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-disabled-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
|
||||
.layout-menu-item-start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
color: @gray;
|
||||
font-family: system-ui, -apple-system, monospace;
|
||||
font-size: 14px;
|
||||
padding-right: 0;
|
||||
padding-left: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.shortcut-symbol {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.pdf-detach-survey {
|
||||
display: flex;
|
||||
font-size: @font-size-small;
|
||||
}
|
||||
|
||||
.pdf-detach-survey-text {
|
||||
margin-left: @margin-sm;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.layout-dropdown-list {
|
||||
a {
|
||||
i {
|
||||
margin-right: @margin-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue