Skip to content

Commit

Permalink
ToolbarGroup - Typescript (#54317)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
margolisj authored Sep 15, 2023
1 parent d2e3e29 commit e534961
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 45 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 7 additions & 6 deletions packages/components/src/dropdown-menu/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { ReactNode } from 'react';
import type { HTMLAttributes, ReactNode } from 'react';
/**
* Internal dependencies
*/
Expand All @@ -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' ];
/**
Expand All @@ -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.
*/
Expand All @@ -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 = {
Expand All @@ -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<
Expand All @@ -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"
*/
Expand Down
8 changes: 3 additions & 5 deletions packages/components/src/toolbar/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,8 @@ Default.args = {
<ToolbarButton icon={ link } label="Link" />
<ToolbarGroup
isCollapsed
// @ts-expect-error TODO: Remove when ToolbarGroup is typed
icon={ false }
label="More rich text controls"
icon={ null }
title="More rich text controls"
controls={ [
{ icon: code, title: 'Inline code' },
{ icon: <InlineImageIcon />, title: 'Inline image' },
Expand All @@ -131,9 +130,8 @@ Default.args = {
/>
</ToolbarGroup>
<ToolbarGroup
// @ts-expect-error TODO: Remove when ToolbarGroup is typed
icon={ more }
label="Align"
title="Align"
isCollapsed
controls={ [
{
Expand Down
20 changes: 13 additions & 7 deletions packages/components/src/toolbar/test/toolbar-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { fireEvent, render, screen } from '@testing-library/react';
*/
import { ToolbarGroup } from '..';

/**
* WordPress dependencies
*/
import { wordpress } from '@wordpress/icons';

describe( 'ToolbarGroup', () => {
describe( 'basic rendering', () => {
it( 'should render an empty node, when controls are not passed', () => {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -63,14 +69,14 @@ describe( 'ToolbarGroup', () => {
[
// First set.
{
icon: 'wordpress',
icon: wordpress,
title: 'WordPress',
},
],
[
// Second set.
{
icon: 'wordpress',
icon: wordpress,
title: 'WordPress',
},
],
Expand All @@ -95,7 +101,7 @@ describe( 'ToolbarGroup', () => {
const clickHandler = jest.fn();
const controls = [
{
icon: 'wordpress',
icon: wordpress,
title: 'WordPress',
onClick: clickHandler,
isActive: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// @ts-nocheck

/**
* External dependencies
*/
Expand All @@ -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
Expand All @@ -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 = [],
Expand All @@ -55,7 +58,7 @@ function ToolbarGroup( {
isCollapsed,
title,
...props
} ) {
}: ToolbarGroupProps ) {
// It'll contain state if `ToolbarGroup` is being used within
// `<Toolbar label="label" />`
const accessibleToolbarState = useContext( ToolbarContext );
Expand All @@ -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 ) {
Expand All @@ -94,13 +99,13 @@ function ToolbarGroup( {
return (
<ToolbarGroupContainer className={ finalClassName } { ...props }>
{ controlSets?.flatMap( ( controlSet, indexOfSet ) =>
controlSet.map( ( control, indexOfControl ) => (
controlSet.map( ( control, indexOfControl: number ) => (
<ToolbarButton
key={ [ indexOfSet, indexOfControl ].join() }
containerClassName={
indexOfSet > 0 && indexOfControl === 0
? 'has-left-divider'
: null
: undefined
}
{ ...control }
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// @ts-nocheck

/**
* WordPress dependencies
*/
Expand All @@ -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
// `<Toolbar label="label" />`
const accessibleToolbarState = useContext( ToolbarContext );

const renderDropdownMenu = ( internalToggleProps ) => (
const renderDropdownMenu = (
internalToggleProps?: DropdownMenuProps[ 'toggleProps' ]
) => (
<DropdownMenu
controls={ controls }
toggleProps={ {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 > ) => (
<div className={ className } { ...props }>
{ children }
</div>
);
export default ToolbarGroupContainer;
92 changes: 92 additions & 0 deletions packages/components/src/toolbar/toolbar-group/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
10 changes: 9 additions & 1 deletion packages/components/src/toolbar/toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@ function UnforwardedToolbar(
alternative: 'ToolbarGroup component',
link: 'https://developer.wordpress.org/block-editor/components/toolbar/',
} );
return <ToolbarGroup { ...props } className={ className } />;
// Extracting title from `props` because `ToolbarGroup` doesn't accept it.
const { title: _title, ...restProps } = props;
return (
<ToolbarGroup
isCollapsed={ false }
{ ...restProps }
className={ className }
/>
);
}
// `ToolbarGroup` already uses components-toolbar for compatibility reasons.
const finalClassName = classnames(
Expand Down

0 comments on commit e534961

Please sign in to comment.