Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* WordPress dependencies
*/
import { Modal } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
displayShortcutList,
shortcutAriaLabel,
type WPKeycodeModifier,
} from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import './style.scss';

interface KeyCombination {
/** Modifier for cross-platform display (e.g. 'primary', 'primaryShift', 'shift'). */
modifier?: WPKeycodeModifier;
/** The key character(s) to display. Pass an array to render each as its own key. */
character: string | string[];
/** Optional aria-label override (used when character is a symbol). */
ariaLabel?: string;
}

interface ShortcutEntry {
description: string;
keyCombination: KeyCombination;
}

const SHORTCUTS: ShortcutEntry[] = [
{
description: __( 'Undo' ),
keyCombination: { modifier: 'primary', character: 'z' },
},
{
description: __( 'Redo' ),
keyCombination: { modifier: 'primaryShift', character: 'z' },
},
{
description: __( 'Pan' ),
keyCombination: {
character: [ '↑', '↓', '←', '→' ],
ariaLabel: __( 'Arrow keys' ),
},
},
{
description: __( 'Zoom in' ),
keyCombination: { character: '+' },
},
{
description: __( 'Zoom out' ),
keyCombination: { character: '-' },
},
{
description: __( 'Rotate 90° clockwise' ),
keyCombination: { character: 'R' },
},
{
description: __( 'Flip horizontal' ),
keyCombination: { character: 'H' },
},
{
description: __( 'Flip vertical' ),
keyCombination: { character: 'V' },
},
{
description: __( 'Resize crop (large step)' ),
keyCombination: {
modifier: 'shift',
character: [ '↑', '↓', '←', '→' ],
ariaLabel: __( 'Shift + Arrow keys' ),
},
},
];

function KeyCombinationDisplay( {
keyCombination,
}: {
keyCombination: KeyCombination;
} ) {
const { modifier, character, ariaLabel } = keyCombination;

let keys: string[];
if ( Array.isArray( character ) ) {
if ( modifier ) {
// Get the modifier prefix (e.g. ['⇧', '+']) from the first char,
// then replace the last element with all the individual chars.
const sample = displayShortcutList[ modifier ]( character[ 0 ] );
keys = [ ...sample.slice( 0, -1 ), ...character ];
} else {
keys = character;
}
} else {
keys = modifier
? displayShortcutList[ modifier ]( character )
: [ character ];
}

const charString = Array.isArray( character )
? character.join( '' )
: character;
let label: string;
if ( ariaLabel ) {
label = ariaLabel;
} else if ( modifier ) {
label = shortcutAriaLabel[ modifier ]( charString );
} else {
label = charString;
}

return (
<kbd
className="media-editor-keyboard-shortcuts-modal__shortcut-term"
aria-label={ label }
>
{ keys.map( ( key, index ) =>
key === '+' && modifier ? (
<Fragment key={ index }>{ key }</Fragment>
) : (
<kbd
key={ index }
className="media-editor-keyboard-shortcuts-modal__shortcut-key"
>
{ key }
</kbd>
)
) }
</kbd>
);
}

interface MediaEditorKeyboardShortcutsModalProps {
onClose: () => void;
}

export default function MediaEditorKeyboardShortcutsModal( {
onClose,
}: MediaEditorKeyboardShortcutsModalProps ) {
return (
<Modal
className="media-editor-keyboard-shortcuts-modal"
title={ __( 'Keyboard shortcuts' ) }
onRequestClose={ onClose }
>
<p className="media-editor-keyboard-shortcuts-modal__note">
{ __(
'These shortcuts work when the image editor has focus.'
) }
</p>
{ /*
* Disable reason: The `list` ARIA role is redundant but
* Safari+VoiceOver won't announce the list otherwise.
*/ }
{ /* eslint-disable jsx-a11y/no-redundant-roles */ }
<ul
className="media-editor-keyboard-shortcuts-modal__shortcut-list"
role="list"
>
Comment thread
ramonjd marked this conversation as resolved.
{ SHORTCUTS.map( ( { description, keyCombination }, index ) => (
<li
key={ index }
className="media-editor-keyboard-shortcuts-modal__shortcut"
>
<span className="media-editor-keyboard-shortcuts-modal__shortcut-description">
{ description }
</span>
<KeyCombinationDisplay
keyCombination={ keyCombination }
/>
</li>
) ) }
</ul>
{ /* eslint-enable jsx-a11y/no-redundant-roles */ }
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@use "@wordpress/base-styles/colors" as *;
@use "@wordpress/base-styles/variables" as *;

.media-editor-keyboard-shortcuts-modal {
min-width: 280px;

.media-editor-keyboard-shortcuts-modal__note {
margin: 0 0 $grid-unit-20;
color: $gray-700;
font-size: 13px;
}

.media-editor-keyboard-shortcuts-modal__shortcut-list {
list-style: none;
margin: 0;
padding: 0;
}

.media-editor-keyboard-shortcuts-modal__shortcut {
display: flex;
align-items: baseline;
padding: 0.6rem 0;
border-top: 1px solid $gray-300;
margin-bottom: 0;

&:last-child {
border-bottom: 1px solid $gray-300;
}
}

.media-editor-keyboard-shortcuts-modal__shortcut-description {
flex: 1;
margin: 0;
}

.media-editor-keyboard-shortcuts-modal__shortcut-term {
background: none;
font-weight: 600;
margin: 0 0 0 1rem;
padding: 0;
text-align: right;
}

.media-editor-keyboard-shortcuts-modal__shortcut-key {
padding: 0.25rem 0.5rem;
border-radius: 8%;
margin: 0 0.2rem 0 0.2rem;

&:last-child {
margin: 0 0 0 0.2rem;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
useState,
} from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { close, drawerRight } from '@wordpress/icons';
import { close, drawerRight, keyboard } from '@wordpress/icons';
import { isAppleOS, isKeyboardEvent } from '@wordpress/keycodes';
import { SnackbarNotices, store as noticesStore } from '@wordpress/notices';
import type { Field } from '@wordpress/dataviews';
Expand Down Expand Up @@ -55,6 +55,7 @@ import { CropperProvider, useCropper } from '../../image-editor';
import type { AspectRatioPreset } from '../../image-editor/core/constants';
import { CROP_CONTROL_ATTR } from '../../hooks/use-crop-gesture-handlers';
import { buildModifiers } from './build-modifiers';
import MediaEditorKeyboardShortcutsModal from '../media-editor-keyboard-shortcuts-modal';

// Details-tab edits the modal bundles into a transformed `/edit` request.
// Matches Core's `WP_REST_Attachments_Controller::get_edit_media_item_args`
Expand Down Expand Up @@ -161,13 +162,20 @@ function HeaderActions( {
onSave,
}: HeaderActionsProps ) {
const saveDisabled = isSaving || ! hasMedia || ! hasChanges;
const [ isShortcutsModalOpen, setIsShortcutsModalOpen ] = useState( false );
return (
<Flex
className="media-editor-modal__header-actions"
justify="flex-end"
expanded={ false }
gap={ 2 }
>
<Button
size="compact"
icon={ keyboard }
label={ __( 'Keyboard shortcuts' ) }
onClick={ () => setIsShortcutsModalOpen( true ) }
/>
<PinnedItems.Slot scope="media-editor" />
<Button
size="compact"
Expand Down Expand Up @@ -196,6 +204,11 @@ function HeaderActions( {
disabled={ isSaving }
accessibleWhenDisabled
/>
{ isShortcutsModalOpen && (
<MediaEditorKeyboardShortcutsModal
onClose={ () => setIsShortcutsModalOpen( false ) }
/>
) }
</Flex>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export interface CropperInteractionActions {
setZoomAtPoint: ( zoom: number, pan: NormalizedPoint ) => void;
/** Snap rotate by 90 degrees. */
snapRotate90: ( direction: 1 | -1 ) => void;
/** Toggle flip on the given axis. */
toggleFlip?: ( direction: 'horizontal' | 'vertical' ) => void;
}

/**
Expand Down Expand Up @@ -876,6 +878,24 @@ export class InteractionController {
this.options.actions.snapRotate90( 1 );
break;
}
case 'h':
case 'H': {
if ( e.metaKey || e.ctrlKey || e.altKey || e.shiftKey ) {
break;
}
e.preventDefault();
this.options.actions.toggleFlip?.( 'horizontal' );
break;
}
case 'v':
case 'V': {
if ( e.metaKey || e.ctrlKey || e.altKey || e.shiftKey ) {
break;
}
e.preventDefault();
this.options.actions.toggleFlip?.( 'vertical' );
break;
Comment thread
ramonjd marked this conversation as resolved.
}
}
}

Expand Down
Loading
Loading