From e5349611de3523240ce169dd7132ecf753c71481 Mon Sep 17 00:00:00 2001
From: margolisj <1588194+margolisj@users.noreply.github.com>
Date: Fri, 15 Sep 2023 13:01:56 -0400
Subject: [PATCH] ToolbarGroup - Typescript (#54317)
* First pass pulling from previous work I royally borked
* Adds missing attribute to dropdown menu types
* Spelling
* Adding some ignores and removes some others.
* Onclick event to dropdown-menu onClick prop.
* Makes data-toolbar-item optional on dropdown props
* Fixes story inconsistencies
* JSDoc changes
* Updates ToolbarGroupControls types w/ dropdown types
* Toolbar-group tests
* Toolbar-group types cleanup
* Toolbar group internal props / toggleProps from dropdown
* Toolbar group extraction
* isNested toolbar group first pass
* Updates types and null to undefined in toolbar-group
* Cleans up dead code, changes dropdown menu role type, swaps some comments around
* Adds changelog reference
* Weird fat finger another dash, removing
---
packages/components/CHANGELOG.md | 1 +
.../components/src/dropdown-menu/types.ts | 13 +--
.../src/toolbar/stories/index.story.tsx | 8 +-
.../src/toolbar/test/toolbar-group.tsx | 20 ++--
.../toolbar-group/{index.js => index.tsx} | 33 ++++---
...llapsed.js => toolbar-group-collapsed.tsx} | 14 ++-
.../toolbar-group/toolbar-group-container.js | 8 --
.../toolbar-group/toolbar-group-container.tsx | 16 ++++
.../src/toolbar/toolbar-group/types.ts | 92 +++++++++++++++++++
.../components/src/toolbar/toolbar/index.tsx | 10 +-
10 files changed, 170 insertions(+), 45 deletions(-)
rename packages/components/src/toolbar/toolbar-group/{index.js => index.tsx} (71%)
rename packages/components/src/toolbar/toolbar-group/{toolbar-group-collapsed.js => toolbar-group-collapsed.tsx} (70%)
delete mode 100644 packages/components/src/toolbar/toolbar-group/toolbar-group-container.js
create mode 100644 packages/components/src/toolbar/toolbar-group/toolbar-group-container.tsx
create mode 100644 packages/components/src/toolbar/toolbar-group/types.ts
diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index d79a5de61c63ec..0aefc8941fe159 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -36,6 +36,7 @@
- `IsolatedEventContainer`: Convert unit test to TypeScript ([#54316](https://github.com/WordPress/gutenberg/pull/54316)).
- `Popover`: Remove `scroll` and `resize` listeners for iframe overflow parents and rely on recently added native Floating UI support ([#54286](https://github.com/WordPress/gutenberg/pull/54286)).
- `Button`: Update documentation to remove the button `focus` prop ([#54397](https://github.com/WordPress/gutenberg/pull/54397)).
+- `Toolbar/ToolbarGroup`: Convert component to TypeScript ([#54317](https://github.com/WordPress/gutenberg/pull/54317)).
### Experimental
diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts
index 2e70557a7fe43e..7b141c97acf580 100644
--- a/packages/components/src/dropdown-menu/types.ts
+++ b/packages/components/src/dropdown-menu/types.ts
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import type { ReactNode } from 'react';
+import type { HTMLAttributes, ReactNode } from 'react';
/**
* Internal dependencies
*/
@@ -13,7 +13,7 @@ import type { NavigableMenuProps } from '../navigable-container/types';
export type DropdownOption = {
/**
- * The Dashicon icon slug to be shown for the option.
+ * The icon to be shown for the option.
*/
icon?: IconProps[ 'icon' ];
/**
@@ -29,7 +29,7 @@ export type DropdownOption = {
/**
* A callback function to invoke when the option is selected.
*/
- onClick?: () => void;
+ onClick?: ( event?: React.MouseEvent ) => void;
/**
* Whether or not the control is currently active.
*/
@@ -41,7 +41,7 @@ export type DropdownOption = {
/**
* The role to apply to the option's HTML element
*/
- role?: HTMLElement[ 'role' ];
+ role?: HTMLAttributes< HTMLElement >[ 'role' ];
};
type DropdownCallbackProps = {
@@ -50,7 +50,7 @@ type DropdownCallbackProps = {
onClose: () => void;
};
-// Manually including `as` prop because `WordPressComponentProps` polymorhpism
+// Manually including `as` prop because `WordPressComponentProps` polymorphism
// creates a union that is too large for TypeScript to handle.
type ToggleProps = Partial<
Omit<
@@ -59,11 +59,12 @@ type ToggleProps = Partial<
>
> & {
as?: React.ElementType | keyof JSX.IntrinsicElements;
+ 'data-toolbar-item'?: boolean;
};
export type DropdownMenuProps = {
/**
- * The Dashicon icon slug to be shown in the collapsed menu button.
+ * The icon to be shown in the collapsed menu button.
*
* @default "menu"
*/
diff --git a/packages/components/src/toolbar/stories/index.story.tsx b/packages/components/src/toolbar/stories/index.story.tsx
index 643c32b6909e0e..2eecf54a4a4939 100644
--- a/packages/components/src/toolbar/stories/index.story.tsx
+++ b/packages/components/src/toolbar/stories/index.story.tsx
@@ -117,9 +117,8 @@ Default.args = {
, title: 'Inline image' },
@@ -131,9 +130,8 @@ Default.args = {
/>
{
describe( 'basic rendering', () => {
it( 'should render an empty node, when controls are not passed', () => {
@@ -23,10 +28,11 @@ describe( 'ToolbarGroup', () => {
} );
it( 'should render a list of controls with buttons', () => {
- const clickHandler = ( event: Event ) => event;
+ const clickHandler = ( event?: React.MouseEvent ) => event;
+
const controls = [
{
- icon: 'wordpress',
+ icon: wordpress,
title: 'WordPress',
onClick: clickHandler,
isActive: false,
@@ -41,10 +47,10 @@ describe( 'ToolbarGroup', () => {
} );
it( 'should render a list of controls with buttons and active control', () => {
- const clickHandler = ( event: Event ) => event;
+ const clickHandler = ( event?: React.MouseEvent ) => event;
const controls = [
{
- icon: 'wordpress',
+ icon: wordpress,
title: 'WordPress',
onClick: clickHandler,
isActive: true,
@@ -63,14 +69,14 @@ describe( 'ToolbarGroup', () => {
[
// First set.
{
- icon: 'wordpress',
+ icon: wordpress,
title: 'WordPress',
},
],
[
// Second set.
{
- icon: 'wordpress',
+ icon: wordpress,
title: 'WordPress',
},
],
@@ -95,7 +101,7 @@ describe( 'ToolbarGroup', () => {
const clickHandler = jest.fn();
const controls = [
{
- icon: 'wordpress',
+ icon: wordpress,
title: 'WordPress',
onClick: clickHandler,
isActive: true,
diff --git a/packages/components/src/toolbar/toolbar-group/index.js b/packages/components/src/toolbar/toolbar-group/index.tsx
similarity index 71%
rename from packages/components/src/toolbar/toolbar-group/index.js
rename to packages/components/src/toolbar/toolbar-group/index.tsx
index c86954e04929a1..e2c0b5b2f22c41 100644
--- a/packages/components/src/toolbar/toolbar-group/index.js
+++ b/packages/components/src/toolbar/toolbar-group/index.tsx
@@ -1,5 +1,3 @@
-// @ts-nocheck
-
/**
* External dependencies
*/
@@ -17,6 +15,11 @@ import ToolbarButton from '../toolbar-button';
import ToolbarGroupContainer from './toolbar-group-container';
import ToolbarGroupCollapsed from './toolbar-group-collapsed';
import ToolbarContext from '../toolbar-context';
+import type { ToolbarGroupProps, ToolbarGroupControls } from './types';
+
+function isNestedArray< T = any >( arr: T[] | T[][] ): arr is T[][] {
+ return Array.isArray( arr ) && Array.isArray( arr[ 0 ] );
+}
/**
* Renders a collapsible group of controls
@@ -41,12 +44,12 @@ import ToolbarContext from '../toolbar-context';
* Either `controls` or `children` is required, otherwise this components
* renders nothing.
*
- * @param {Object} props Component props.
- * @param {Array} [props.controls] The controls to render in this toolbar.
- * @param {WPElement} [props.children] Any other things to render inside the toolbar besides the controls.
- * @param {string} [props.className] Class to set on the container div.
- * @param {boolean} [props.isCollapsed] Turns ToolbarGroup into a dropdown menu.
- * @param {string} [props.title] ARIA label for dropdown menu if is collapsed.
+ * @param props Component props.
+ * @param [props.controls] The controls to render in this toolbar.
+ * @param [props.children] Any other things to render inside the toolbar besides the controls.
+ * @param [props.className] Class to set on the container div.
+ * @param [props.isCollapsed] Turns ToolbarGroup into a dropdown menu.
+ * @param [props.title] ARIA label for dropdown menu if is collapsed.
*/
function ToolbarGroup( {
controls = [],
@@ -55,7 +58,7 @@ function ToolbarGroup( {
isCollapsed,
title,
...props
-} ) {
+}: ToolbarGroupProps ) {
// It'll contain state if `ToolbarGroup` is being used within
// ``
const accessibleToolbarState = useContext( ToolbarContext );
@@ -74,9 +77,11 @@ function ToolbarGroup( {
);
// Normalize controls to nested array of objects (sets of controls)
- let controlSets = controls;
- if ( ! Array.isArray( controlSets[ 0 ] ) ) {
- controlSets = [ controlSets ];
+ let controlSets: ToolbarGroupControls[][];
+ if ( isNestedArray( controls ) ) {
+ controlSets = controls;
+ } else {
+ controlSets = [ controls ];
}
if ( isCollapsed ) {
@@ -94,13 +99,13 @@ function ToolbarGroup( {
return (
{ controlSets?.flatMap( ( controlSet, indexOfSet ) =>
- controlSet.map( ( control, indexOfControl ) => (
+ controlSet.map( ( control, indexOfControl: number ) => (
0 && indexOfControl === 0
? 'has-left-divider'
- : null
+ : undefined
}
{ ...control }
/>
diff --git a/packages/components/src/toolbar/toolbar-group/toolbar-group-collapsed.js b/packages/components/src/toolbar/toolbar-group/toolbar-group-collapsed.tsx
similarity index 70%
rename from packages/components/src/toolbar/toolbar-group/toolbar-group-collapsed.js
rename to packages/components/src/toolbar/toolbar-group/toolbar-group-collapsed.tsx
index 147e610bb0624f..a829aa3b001442 100644
--- a/packages/components/src/toolbar/toolbar-group/toolbar-group-collapsed.js
+++ b/packages/components/src/toolbar/toolbar-group/toolbar-group-collapsed.tsx
@@ -1,5 +1,3 @@
-// @ts-nocheck
-
/**
* WordPress dependencies
*/
@@ -11,13 +9,21 @@ import { useContext } from '@wordpress/element';
import DropdownMenu from '../../dropdown-menu';
import ToolbarContext from '../toolbar-context';
import ToolbarItem from '../toolbar-item';
+import type { ToolbarGroupCollapsedProps } from './types';
+import type { DropdownMenuProps } from '../../dropdown-menu/types';
-function ToolbarGroupCollapsed( { controls = [], toggleProps, ...props } ) {
+function ToolbarGroupCollapsed( {
+ controls = [],
+ toggleProps,
+ ...props
+}: ToolbarGroupCollapsedProps ) {
// It'll contain state if `ToolbarGroup` is being used within
// ``
const accessibleToolbarState = useContext( ToolbarContext );
- const renderDropdownMenu = ( internalToggleProps ) => (
+ const renderDropdownMenu = (
+ internalToggleProps?: DropdownMenuProps[ 'toggleProps' ]
+ ) => (
(
-
- { children }
-
-);
-export default ToolbarGroupContainer;
diff --git a/packages/components/src/toolbar/toolbar-group/toolbar-group-container.tsx b/packages/components/src/toolbar/toolbar-group/toolbar-group-container.tsx
new file mode 100644
index 00000000000000..faddda8a83101a
--- /dev/null
+++ b/packages/components/src/toolbar/toolbar-group/toolbar-group-container.tsx
@@ -0,0 +1,16 @@
+/**
+ * Internal dependencies
+ */
+import type { WordPressComponentProps } from '../../ui/context';
+import type { ToolbarGroupContainerProps } from './types';
+
+const ToolbarGroupContainer = ( {
+ className,
+ children,
+ ...props
+}: WordPressComponentProps< ToolbarGroupContainerProps, 'div', false > ) => (
+
+ { children }
+
+);
+export default ToolbarGroupContainer;
diff --git a/packages/components/src/toolbar/toolbar-group/types.ts b/packages/components/src/toolbar/toolbar-group/types.ts
new file mode 100644
index 00000000000000..6475f3cf927f4a
--- /dev/null
+++ b/packages/components/src/toolbar/toolbar-group/types.ts
@@ -0,0 +1,92 @@
+/**
+ * External dependencies
+ */
+import type { ReactNode } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import type {
+ DropdownMenuProps,
+ DropdownOption,
+} from '../../dropdown-menu/types';
+
+/**
+ * WordPress dependencies
+ */
+import type { Props as IconProps } from '../../icon';
+
+export type ToolbarGroupControls = DropdownOption & {
+ /**
+ * An optional subscript associated to the control.
+ */
+ subscript?: string;
+};
+
+type ToolbarGroupPropsBase = {
+ /**
+ * The controls to render in this toolbar.
+ */
+ controls?: ToolbarGroupControls[] | ToolbarGroupControls[][];
+
+ /**
+ * Class to set on the container div.
+ */
+ className?: string;
+
+ /**
+ * Any other things to render inside the toolbar besides the controls.
+ */
+ children?: ReactNode;
+
+ /**
+ * The Dashicon icon slug to be shown for the option.
+ */
+ icon?: IconProps[ 'icon' ];
+};
+
+export type ToolbarGroupProps = ToolbarGroupPropsBase &
+ (
+ | {
+ /**
+ * When true, turns `ToolbarGroup` into a dropdown menu.
+ */
+ isCollapsed?: false;
+ /**
+ * Any other things to render inside the toolbar besides the controls.
+ */
+ children?: ReactNode;
+ title?: never;
+ }
+ | {
+ /**
+ * When true, turns `ToolbarGroup` into a dropdown menu.
+ */
+ isCollapsed: true;
+ /**
+ * Any other things to render inside the toolbar besides the controls.
+ */
+ children?: ToolbarGroupCollapsedProps[ 'children' ];
+ /**
+ * ARIA label for dropdown menu if is collapsed.
+ */
+ title: string;
+ }
+ );
+
+export type ToolbarGroupCollapsedProps = DropdownMenuProps;
+
+export type ToolbarGroupContainerProps = {
+ /**
+ * Children to be rendered inside the toolbar.
+ */
+ children?: ReactNode;
+ /**
+ * Class to set on the container div.
+ */
+ className?: string;
+ /**
+ * Props to be passed.
+ */
+ props?: any;
+};
diff --git a/packages/components/src/toolbar/toolbar/index.tsx b/packages/components/src/toolbar/toolbar/index.tsx
index 96e35d399df944..2eac5c5e614e3b 100644
--- a/packages/components/src/toolbar/toolbar/index.tsx
+++ b/packages/components/src/toolbar/toolbar/index.tsx
@@ -42,7 +42,15 @@ function UnforwardedToolbar(
alternative: 'ToolbarGroup component',
link: 'https://developer.wordpress.org/block-editor/components/toolbar/',
} );
- return ;
+ // Extracting title from `props` because `ToolbarGroup` doesn't accept it.
+ const { title: _title, ...restProps } = props;
+ return (
+
+ );
}
// `ToolbarGroup` already uses components-toolbar for compatibility reasons.
const finalClassName = classnames(