Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ToolbarGroup - Typescript #54317

Merged
merged 19 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
margolisj marked this conversation as resolved.
Show resolved Hide resolved
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
margolisj marked this conversation as resolved.
Show resolved Hide resolved
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
Loading