diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.js index 2323b79a20..4d50e3a6b9 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.js +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.js @@ -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 @@ -47,27 +48,46 @@ function IconCheckmark({ iconFor, pdfLayout, view, detachRole }) { return } -function PdfDetachMenuItem({ handleDetach, children }) { - const { t } = useTranslation() - - if (!('BroadcastChannel' in window)) { - return ( - - {children} - - ) - } - - return {children} +function LayoutMenuItem({ checkmark, icon, text, shortcut, ...props }) { + return ( + +
+
+
{checkmark}
+
{icon}
+
{text}
+
+ +
+
+ ) +} +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 ( + + } + icon={} + text={t('pdf_in_separate_tab')} + shortcut="Control+Option+ArrowUp" + /> + + ) } 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() { - - handleChangeLayout('sideBySide')}> - - - {t('editor_and_pdf')} - + + handleChangeLayout('sideBySide')} + checkmark={ + + } + icon={} + text={t('editor_and_pdf')} + shortcut="Control+Option+ArrowDown" + /> - handleChangeLayout('flat', 'editor')} - className="menu-item-with-svg" - > - - - - - + checkmark={ + + } + icon={ + + + + } + text={ , ]} /> - - + } + shortcut="Control+Option+ArrowLeft" + /> - handleChangeLayout('flat', 'pdf')} - className="menu-item-with-svg" - > - - - - - + checkmark={ + + } + icon={ + + + + } + text={ , ]} /> - - + } + shortcut="Control+Option+ArrowRight" + /> - {detachRole === 'detacher' ? ( - - {detachIsLinked ? : } - - {t('pdf_in_separate_tab')} - + {'BroadcastChannel' in window ? ( + handleDetach()} + checkmark={ + detachRole === 'detacher' ? ( + detachIsLinked ? ( + + ) : ( + + ) + ) : ( + + ) + } + icon={} + text={t('pdf_in_separate_tab')} + shortcut="Control+Option+ArrowUp" + /> ) : ( - - - - {t('pdf_in_separate_tab')} - + )} @@ -232,7 +295,7 @@ function LayoutDropdownButton() { ) } -export default LayoutDropdownButton +export default memo(LayoutDropdownButton) IconCheckmark.propTypes = { iconFor: PropTypes.string.isRequired, diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/shortcut.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/shortcut.tsx new file mode 100644 index 0000000000..6a10093602 --- /dev/null +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/shortcut.tsx @@ -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 ( + + ) +} + +export default memo(Shortcut) diff --git a/services/web/frontend/stylesheets/app/editor/toolbar.less b/services/web/frontend/stylesheets/app/editor/toolbar.less index b8eaa4c7d1..19a51714af 100644 --- a/services/web/frontend/stylesheets/app/editor/toolbar.less +++ b/services/web/frontend/stylesheets/app/editor/toolbar.less @@ -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; - } - } - } - } -} diff --git a/services/web/frontend/stylesheets/components/dropdowns.less b/services/web/frontend/stylesheets/components/dropdowns.less index 45d351c745..522b315feb 100755 --- a/services/web/frontend/stylesheets/components/dropdowns.less +++ b/services/web/frontend/stylesheets/components/dropdowns.less @@ -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; + } + } + } +}