Skip to content

Commit

Permalink
KeyboardShortcuts: Convert to TypeScript (#47429)
Browse files Browse the repository at this point in the history
* Rename file

* KeyboardShortcuts: Add types

* Return as valid JSX element

* Convert tests

* Add main JSDoc

* Add story

* Add changelog

* Use `createEvent()`

Co-authored-by: flootr <[email protected]>

* Add todo comment

---------

Co-authored-by: flootr <[email protected]>
  • Loading branch information
mirka and flootr authored Jan 30, 2023
1 parent f678550 commit 01cb2bd
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 61 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- `DropdownMenu`: migrate Storybook to controls ([47149](https://github.com/WordPress/gutenberg/pull/47149)).
- Removed deprecated `@storybook/addon-knobs` dependency from the package ([47152](https://github.com/WordPress/gutenberg/pull/47152)).
- `ColorListPicker`: Convert to TypeScript ([#46358](https://github.com/WordPress/gutenberg/pull/46358)).
- `KeyboardShortcuts`: Convert to TypeScript ([#47429](https://github.com/WordPress/gutenberg/pull/47429)).
- `ColorPalette`, `BorderControl`, `GradientPicker`: refine types and logic around single vs multiple palettes
([#47384](https://github.com/WordPress/gutenberg/pull/47384)).
- `Button`: Convert to TypeScript ([#46997](https://github.com/WordPress/gutenberg/pull/46997)).
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/keyboard-shortcuts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The component accepts the following props:
Elements to render, upon whom key events are to be monitored.
- Type: `Element` | `Element[]`
- Type: `ReactNode`
- Required: No
### shortcuts
Expand Down
53 changes: 0 additions & 53 deletions packages/components/src/keyboard-shortcuts/index.js

This file was deleted.

93 changes: 93 additions & 0 deletions packages/components/src/keyboard-shortcuts/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* WordPress dependencies
*/
import { useRef, Children } from '@wordpress/element';
import { useKeyboardShortcut } from '@wordpress/compose';

/**
* Internal dependencies
*/
import type { KeyboardShortcutProps, KeyboardShortcutsProps } from './types';

function KeyboardShortcut( {
target,
callback,
shortcut,
bindGlobal,
eventName,
}: KeyboardShortcutProps ) {
useKeyboardShortcut( shortcut, callback, {
bindGlobal,
target,
eventName,
} );

return null;
}

/**
* `KeyboardShortcuts` is a component which handles keyboard sequences during the lifetime of the rendering element.
*
* When passed children, it will capture key events which occur on or within the children. If no children are passed, events are captured on the document.
*
* It uses the [Mousetrap](https://craig.is/killing/mice) library to implement keyboard sequence bindings.
*
* ```jsx
* import { KeyboardShortcuts } from '@wordpress/components';
* import { useState } from '@wordpress/element';
*
* const MyKeyboardShortcuts = () => {
* const [ isAllSelected, setIsAllSelected ] = useState( false );
* const selectAll = () => {
* setIsAllSelected( true );
* };
*
* return (
* <div>
* <KeyboardShortcuts
* shortcuts={ {
* 'mod+a': selectAll,
* } }
* />
* [cmd/ctrl + A] Combination pressed? { isAllSelected ? 'Yes' : 'No' }
* </div>
* );
* };
* ```
*/
function KeyboardShortcuts( {
children,
shortcuts,
bindGlobal,
eventName,
}: KeyboardShortcutsProps ) {
const target = useRef( null );

const element = Object.entries( shortcuts ?? {} ).map(
( [ shortcut, callback ] ) => (
<KeyboardShortcut
key={ shortcut }
shortcut={ shortcut }
callback={ callback }
bindGlobal={ bindGlobal }
eventName={ eventName }
target={ target }
/>
)
);

// Render as non-visual if there are no children pressed. Keyboard
// events will be bound to the document instead.
if ( ! Children.count( children ) ) {
return <>{ element }</>;
}

return (
<div ref={ target }>
{ element }
{ children }
</div>
);
}

export default KeyboardShortcuts;
60 changes: 60 additions & 0 deletions packages/components/src/keyboard-shortcuts/stories/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* External dependencies
*/
import type { ComponentMeta, ComponentStory } from '@storybook/react';

/**
* Internal dependencies
*/
import KeyboardShortcuts from '..';

const meta: ComponentMeta< typeof KeyboardShortcuts > = {
component: KeyboardShortcuts,
title: 'Components/KeyboardShortcuts',
parameters: {
controls: { expanded: true },
docs: { source: { state: 'open' } },
},
};
export default meta;

const Template: ComponentStory< typeof KeyboardShortcuts > = ( props ) => (
<KeyboardShortcuts { ...props } />
);

export const Default = Template.bind( {} );
Default.args = {
shortcuts: {
// eslint-disable-next-line no-alert
a: () => window.alert( 'You hit "a"!' ),
// eslint-disable-next-line no-alert
b: () => window.alert( 'You hit "b"!' ),
},
children: (
<div>
<p>{ `Hit the "a" or "b" key in this textarea:` }</p>
<textarea />
</div>
),
};
Default.parameters = {
docs: {
source: {
code: `
<KeyboardShortcuts
shortcuts={{
a: () => window.alert('You hit "a"!'),
b: () => window.alert('You hit "b"!'),
}}
>
<div>
<p>
Hit the "a" or "b" key in this textarea:
</p>
<textarea />
</div>
</KeyboardShortcuts>
`,
},
},
};
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
/**
* External dependencies
*/
import { fireEvent, render, screen } from '@testing-library/react';
import { createEvent, fireEvent, render, screen } from '@testing-library/react';

/**
* Internal dependencies
*/
import KeyboardShortcuts from '../';
import KeyboardShortcuts from '..';

describe( 'KeyboardShortcuts', () => {
function keyPress( which, target ) {
function keyPress(
which: KeyboardEvent[ 'which' ],
target: Parameters< typeof fireEvent >[ 0 ]
) {
[ 'keydown', 'keypress', 'keyup' ].forEach( ( eventName ) => {
const event = new window.Event( eventName, { bubbles: true } );
event.keyCode = which;
event.which = which;
const event = createEvent(
eventName,
target,
{
bubbles: true,
keyCode: which,
which,
},
{ EventType: 'KeyboardEvent' }
);
fireEvent( target, event );
} );
}
Expand Down
51 changes: 51 additions & 0 deletions packages/components/src/keyboard-shortcuts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* WordPress dependencies
*/
import type { useKeyboardShortcut } from '@wordpress/compose';

// TODO: We wouldn't have to do this if this type was exported from `@wordpress/compose`.
type WPKeyboardShortcutConfig = NonNullable<
Parameters< typeof useKeyboardShortcut >[ 2 ]
>;

export type KeyboardShortcutProps = {
shortcut: string | string[];
/**
* @see {@link https://craig.is/killing/mice Mousetrap documentation}
*/
callback: ( event: Mousetrap.ExtendedKeyboardEvent, combo: string ) => void;
} & Pick< WPKeyboardShortcutConfig, 'bindGlobal' | 'eventName' | 'target' >;

export type KeyboardShortcutsProps = {
/**
* Elements to render, upon whom key events are to be monitored.
*/
children?: React.ReactNode;
/**
* An object of shortcut bindings, where each key is a keyboard combination,
* the value of which is the callback to be invoked when the key combination is pressed.
*
* The value of each shortcut should be a consistent function reference, not an anonymous function.
* Otherwise, the callback will not be correctly unbound when the component unmounts.
*
* The `KeyboardShortcuts` component will not update to reflect a changed `shortcuts` prop.
* If you need to change shortcuts, mount a separate `KeyboardShortcuts` element,
* which can be achieved by assigning a unique `key` prop.
*
* @see {@link https://craig.is/killing/mice Mousetrap documentation}
*/
shortcuts: Record< string, KeyboardShortcutProps[ 'callback' ] >;
/**
* By default, a callback will not be invoked if the key combination occurs in an editable field.
* Pass `bindGlobal` as `true` if the key events should be observed globally, including within editable fields.
*
* Tip: If you need some but not all keyboard events to be observed globally,
* simply render two distinct `KeyboardShortcuts` elements, one with and one without the `bindGlobal` prop.
*/
bindGlobal?: KeyboardShortcutProps[ 'bindGlobal' ];
/**
* By default, a callback is invoked in response to the `keydown` event.
* To override this, pass `eventName` with the name of a specific keyboard event.
*/
eventName?: KeyboardShortcutProps[ 'eventName' ];
};
1 change: 0 additions & 1 deletion packages/components/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"src/higher-order/with-filters",
"src/higher-order/with-focus-return",
"src/higher-order/with-notices",
"src/keyboard-shortcuts",
"src/menu-items-choice",
"src/navigation",
"src/notice",
Expand Down

1 comment on commit 01cb2bd

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in 01cb2bd.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4044180045
📝 Reported issues:

Please sign in to comment.