From e95772deec5e150ba779dca7f311c3c9db775846 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Mon, 24 Apr 2023 10:09:54 -0400 Subject: [PATCH 01/19] remove ts-nocheck and rename file --- packages/components/src/dropdown-menu/{index.js => index.tsx} | 1 - 1 file changed, 1 deletion(-) rename packages/components/src/dropdown-menu/{index.js => index.tsx} (99%) diff --git a/packages/components/src/dropdown-menu/index.js b/packages/components/src/dropdown-menu/index.tsx similarity index 99% rename from packages/components/src/dropdown-menu/index.js rename to packages/components/src/dropdown-menu/index.tsx index 481e87cd102b7..aa4020fa5e627 100644 --- a/packages/components/src/dropdown-menu/index.js +++ b/packages/components/src/dropdown-menu/index.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck /** * External dependencies */ From 5112788e2ac9227db61c0d510d4ddb3c313825fc Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Mon, 24 Apr 2023 10:11:36 -0400 Subject: [PATCH 02/19] add types.ts --- packages/components/src/dropdown-menu/types.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/components/src/dropdown-menu/types.ts diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts new file mode 100644 index 0000000000000..e69de29bb2d1d From 0b15b0401e2db15459ef30ab2a65272efe617fca Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Mon, 24 Apr 2023 14:31:13 -0400 Subject: [PATCH 03/19] add component prop types --- .../components/src/dropdown-menu/index.tsx | 3 +- .../components/src/dropdown-menu/types.ts | 153 ++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index aa4020fa5e627..ef11bf4479582 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -14,6 +14,7 @@ import { menu } from '@wordpress/icons'; import Button from '../button'; import Dropdown from '../dropdown'; import { NavigableMenu } from '../navigable-container'; +import type { DropdownMenuProps } from './types'; function mergeProps( defaultProps = {}, props = {} ) { const mergedProps = { @@ -41,7 +42,7 @@ function isFunction( maybeFunc ) { return typeof maybeFunc === 'function'; } -function DropdownMenu( dropdownMenuProps ) { +function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { const { children, className, diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts index e69de29bb2d1d..820d28160af50 100644 --- a/packages/components/src/dropdown-menu/types.ts +++ b/packages/components/src/dropdown-menu/types.ts @@ -0,0 +1,153 @@ +/** + * External dependencies + */ +import type { ReactNode } from 'react'; +/** + * Internal dependencies + */ +import type { ButtonAsButtonProps } from '../button/types'; +import type { WordPressComponentProps } from '../ui/context'; +import type { DropdownProps } from '../dropdown/types'; +import type { Props as IconProps } from '../icon'; + +type DropdownOption = { + /** + * The Dashicon icon slug to be shown for the option. + */ + icon?: IconProps[ 'icon' ]; + /** + * A human-readable title to display for the option. + */ + title: string; + /** + * Whether or not the option is disabled. + * + * @default false + */ + isDisabled?: boolean; + /** + * A callback function to invoke when the option is selected. + */ + onClick?: () => void; + /** + * Whether or not the control is currently active. + */ + isActive?: boolean; + /** + * Text to use for the internal `Button` component's tooltip. + */ + label?: string; + /** + * The role to apply to the option's HTML element + */ + role?: HTMLElement[ 'role' ]; +}; + +type DropdownCallbackProps = { + isOpen: boolean; + onToggle: () => void; + onClose: () => void; +}; + +// Manually including `as` prop because `WordPressComponentProps` polymorhpism +// creates a union that is too large for TypeScript to handle. +type ToggleProps = Partial< + Omit< + WordPressComponentProps< ButtonAsButtonProps, 'button', false >, + 'label' | 'text' + > +> & { + as?: React.ElementType | keyof JSX.IntrinsicElements; +}; + +type MenuProps = { + onNavigate?: ( index: number, child: HTMLElement ) => void; + cycle?: boolean; + orientation?: 'horizontal' | 'vertical' | 'both'; + role?: string; + 'aria-label'?: string; + className?: string; +}; + +export type DropdownMenuProps = { + /** + * The Dashicon icon slug to be shown in the collapsed menu button. + * + * @default "menu" + */ + icon?: IconProps[ 'icon' ] | null; + /** + * A human-readable label to present as accessibility text on the focused + * collapsed menu button. + */ + label: string; + /** + * A class name to apply to the dropdown menu's toggle element wrapper. + */ + className?: string; + /** + * Properties of `popoverProps` object will be passed as props to the nested + * `Popover` component. + * Use this object to modify props available for the `Popover` component that + * are not already exposed in the `DropdownMenu` component, e.g.: the + * direction in which the popover should open relative to its parent node + * set with `position` prop. + */ + popoverProps?: DropdownProps[ 'popoverProps' ]; + /** + * Properties of `toggleProps` object will be passed as props to the nested + * `Button` component in the `renderToggle` implementation of the `Dropdown` + * component used internally. + * Use this object to modify props available for the `Button` component that + * are not already exposed in the `DropdownMenu` component, e.g.: the tooltip + * text displayed on hover set with `tooltip` prop. + */ + toggleProps?: ToggleProps; + /** + * Properties of `menuProps` object will be passed as props to the nested + * `NavigableMenu` component in the `renderContent` implementation of the + * `Dropdown` component used internally. + * Use this object to modify props available for the `NavigableMenu` + * component that are not already exposed in the `DropdownMenu` component, + * e.g.: the orientation of the menu set with `orientation` prop. + */ + // TODO: NavigableContainer is currently being typed. Just import these props from it when they're available. + menuProps?: Omit< MenuProps, 'children' >; + /** + * In some contexts, the arrow down key used to open the dropdown menu might + * need to be disabled—for example when that key is used to perform another + * action. + * + * @default false + */ + disableOpenOnArrowDown?: boolean; + /** + * Text to display on the nested `Button` component in the `renderToggle` + * implementation of the `Dropdown` component used internally. + */ + text?: string; + /** + * Whether or not `no-icons` should be added to the menu's `className`. + */ + noIcons?: boolean; + /** + * A [function render prop](https://reactjs.org/docs/render-props.html#using-props-other-than-render) + * which should return an element or elements valid for use in a DropdownMenu: + * `MenuItem`, `MenuItemsChoice`, or `MenuGroup`. Its first argument is a + * props object including the same values as given to a `Dropdown`'s + * `renderContent` (`isOpen`, `onToggle`, `onClose`). + * + * A valid DropdownMenu must specify a `controls` or `children` prop, or both. + */ + children?: ( callbackProps: DropdownCallbackProps ) => ReactNode; + /** + * An array of objects describing the options to be shown in the expanded + * menu. Each object should include an `icon` Dashicon slug string, a + * human-readable `title` string, `isDisabled` boolean flag, and an `onClick` + * function callback to invoke when the option is selected. + * + * A valid DropdownMenu must specify a `controls` or `children` prop, or both. + */ + + controls?: DropdownOption[]; +}; From e653185382d6a5130cf945d90d474ab56a668023 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:07:49 -0400 Subject: [PATCH 04/19] add types to util functions --- packages/components/src/dropdown-menu/index.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index ef11bf4479582..e43bf26978176 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -16,8 +16,10 @@ import Dropdown from '../dropdown'; import { NavigableMenu } from '../navigable-container'; import type { DropdownMenuProps } from './types'; -function mergeProps( defaultProps = {}, props = {} ) { - const mergedProps = { +function mergeProps< + T extends { className?: string; [ key: string ]: unknown } +>( defaultProps: Partial< T > = {}, props: T = {} as T ) { + const mergedProps: T = { ...defaultProps, ...props, }; @@ -32,13 +34,7 @@ function mergeProps( defaultProps = {}, props = {} ) { return mergedProps; } -/** - * Whether the argument is a function. - * - * @param {*} maybeFunc The argument to check. - * @return {boolean} True if the argument is a function, false otherwise. - */ -function isFunction( maybeFunc ) { +function isFunction( maybeFunc: unknown ): maybeFunc is () => void { return typeof maybeFunc === 'function'; } From c284285352a44d6001dba17adc143e9da2893b1d Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:55:22 -0400 Subject: [PATCH 05/19] type control normalization --- packages/components/src/dropdown-menu/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index e43bf26978176..c0f2c612937c3 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -58,13 +58,18 @@ function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { } // Normalize controls to nested array of objects (sets of controls) - let controlSets; + let controlSets: NonNullable< typeof controls >[]; if ( controls?.length ) { + // @ts-expect-error The check below is needed because `DropdownMenus` + // rendered by `ToolBarGroup` receive controls as a nested array. controlSets = controls; if ( ! Array.isArray( controlSets[ 0 ] ) ) { - controlSets = [ controlSets ]; + // This is not ideal but was introduced to avoid runtime changes, + // see above comment. + controlSets = [ controls as unknown as typeof controls ]; } } + const mergedPopoverProps = mergeProps( { className: 'components-dropdown-menu__popover', From 1a195b377b2f04e7f2eb8c951a46b9471088a847 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Wed, 26 Apr 2023 15:36:23 -0400 Subject: [PATCH 06/19] add remaining type updates --- .../components/src/dropdown-menu/index.tsx | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index c0f2c612937c3..dd95312aa4a00 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -82,7 +82,7 @@ function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { className={ classnames( 'components-dropdown-menu', className ) } popoverProps={ mergedPopoverProps } renderToggle={ ( { isOpen, onToggle } ) => { - const openOnArrowDown = ( event ) => { + const openOnArrowDown = ( event: React.KeyboardEvent ) => { if ( disableOpenOnArrowDown ) { return; } @@ -111,18 +111,22 @@ function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { { - onToggle( event ); - if ( mergedToggleProps.onClick ) { - mergedToggleProps.onClick( event ); - } - } } - onKeyDown={ ( event ) => { - openOnArrowDown( event ); - if ( mergedToggleProps.onKeyDown ) { - mergedToggleProps.onKeyDown( event ); - } - } } + onClick={ + ( ( event ) => { + onToggle(); + if ( mergedToggleProps.onClick ) { + mergedToggleProps.onClick( event ); + } + } ) as React.MouseEventHandler< HTMLButtonElement > + } + onKeyDown={ + ( ( event ) => { + openOnArrowDown( event ); + if ( mergedToggleProps.onKeyDown ) { + mergedToggleProps.onKeyDown( event ); + } + } ) as React.KeyboardEventHandler< HTMLButtonElement > + } aria-haspopup="true" aria-expanded={ isOpen } label={ label } From 59b8f891552f762a9825ce0fed5ce15a266209d6 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Wed, 26 Apr 2023 16:11:58 -0400 Subject: [PATCH 07/19] update `Toolbar` stories --- .../components/src/toolbar/stories/index.tsx | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/components/src/toolbar/stories/index.tsx b/packages/components/src/toolbar/stories/index.tsx index fd0fb9587347d..17a8e64111eb2 100644 --- a/packages/components/src/toolbar/stories/index.tsx +++ b/packages/components/src/toolbar/stories/index.tsx @@ -82,34 +82,31 @@ Default.args = { - { - // @ts-expect-error TODO: Remove when DropdownMenu is typed - ( toggleProps ) => { - return ( - - ); - } - } + { /* There is an issue here with TS not recognizing the + * `RenderProp` being passed. + * @ts-expect-error */ } + { ( toggleProps ) => ( + + ) } From de5984c4d642f7d15936df03805eb458a5fda245 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Wed, 26 Apr 2023 16:18:55 -0400 Subject: [PATCH 08/19] update README --- .../components/src/dropdown-menu/README.md | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index fc6fc4ba708c9..69ecb954b6eb2 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -131,80 +131,71 @@ const MyDropdownMenu = () => ( The component accepts the following props: -#### icon +#### `icon`: `string | null` The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug to be shown in the collapsed menu button. -- Type: `String|null` - Required: No - Default: `"menu"` See also: [https://developer.wordpress.org/resource/dashicons/](https://developer.wordpress.org/resource/dashicons/) -#### label +#### `label`: `string` A human-readable label to present as accessibility text on the focused collapsed menu button. -- Type: `String` - Required: Yes -#### controls +#### `controls:` `DropdownOption[]` An array of objects describing the options to be shown in the expanded menu. Each object should include an `icon` [Dashicon](https://developer.wordpress.org/resource/dashicons/) slug string, a human-readable `title` string, `isDisabled` boolean flag and an `onClick` function callback to invoke when the option is selected. -A valid DropdownMenu must specify one or the other of a `controls` or `children` prop. - -- Type: `Array` +A valid DropdownMenu must specify a `controls` or `children` prop, or both. - Required: No -#### children +#### `children`: `( callbackProps: DropdownCallbackProps ) => ReactNode` A [function render prop](https://reactjs.org/docs/render-props.html#using-props-other-than-render) which should return an element or elements valid for use in a DropdownMenu: `MenuItem`, `MenuItemsChoice`, or `MenuGroup`. Its first argument is a props object including the same values as given to a [`Dropdown`'s `renderContent`](/packages/components/src/dropdown#rendercontent) (`isOpen`, `onToggle`, `onClose`). -A valid DropdownMenu must specify one or the other of a `controls` or `children` prop. +A valid DropdownMenu must specify a `controls` or `children` prop, or both. -- Type: `Function` - Required: No See also: [https://developer.wordpress.org/resource/dashicons/](https://developer.wordpress.org/resource/dashicons/) -#### className +#### `className`: `string` A class name to apply to the dropdown menu's toggle element wrapper. -- Type: `String` - Required: No -#### popoverProps +#### `popoverProps`: `DropdownProps[ 'popoverProps' ]` Properties of `popoverProps` object will be passed as props to the nested `Popover` component. Use this object to modify props available for the `Popover` component that are not already exposed in the `DropdownMenu` component, e.g.: the direction in which the popover should open relative to its parent node set with `position` prop. -- Type: `Object` - Required: No -#### toggleProps +#### `toggleProps`: `Object` Properties of `toggleProps` object will be passed as props to the nested `Button` component in the `renderToggle` implementation of the `Dropdown` component used internally. Use this object to modify props available for the `Button` component that are not already exposed in the `DropdownMenu` component, e.g.: the tooltip text displayed on hover set with `tooltip` prop. -- Type: `Object` - Required: No -#### menuProps +// TODO: Update this when `NavigableMenu` is typed and `menuProps` is imported from there +#### `menuProps`: `Object` Properties of `menuProps` object will be passed as props to the nested `NavigableMenu` component in the `renderContent` implementation of the `Dropdown` component used internally. Use this object to modify props available for the `NavigableMenu` component that are not already exposed in the `DropdownMenu` component, e.g.: the orientation of the menu set with `orientation` prop. -- Type: `Object` - Required: No -#### disableOpenOnArrowDown +#### `disableOpenOnArrowDown`: `boolean` In some contexts, the arrow down key used to open the dropdown menu might need to be disabled—for example when that key is used to perform another action. -- Type: `boolean` - Required: No - Default: `false` From 8955a7b25c97fb59975572b3842cf88593869c52 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:09:45 -0400 Subject: [PATCH 09/19] add example snippet to component export --- .../components/src/dropdown-menu/index.tsx | 81 ++++++++++++ .../src/dropdown-menu/stories/index.js | 121 ------------------ 2 files changed, 81 insertions(+), 121 deletions(-) delete mode 100644 packages/components/src/dropdown-menu/stories/index.js diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index dd95312aa4a00..eead1e193fcd1 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -38,6 +38,87 @@ function isFunction( maybeFunc: unknown ): maybeFunc is () => void { return typeof maybeFunc === 'function'; } +/** + * + * The DropdownMenu displays a list of actions (each contained in a MenuItem, + * MenuItemsChoice, or MenuGroup) in a compact way. It appears in a Popover + * after the user has interacted with an element (a button or icon) or when + * they perform a specific action. + * + * Render a Dropdown Menu with a set of controls: + * + * ```jsx + * import { DropdownMenu } from '@wordpress/components'; + * import { + * more, + * arrowLeft, + * arrowRight, + * arrowUp, + * arrowDown, + * } from '@wordpress/icons'; + * + * const MyDropdownMenu = () => ( + * console.log( 'up' ), + * }, + * { + * title: 'Right', + * icon: arrowRight, + * onClick: () => console.log( 'right' ), + * }, + * { + * title: 'Down', + * icon: arrowDown, + * onClick: () => console.log( 'down' ), + * }, + * { + * title: 'Left', + * icon: arrowLeft, + * onClick: () => console.log( 'left' ), + * }, + * ] } + * /> + * ); + * ``` + * + * Alternatively, specify a `children` function which returns elements valid for + * use in a DropdownMenu: `MenuItem`, `MenuItemsChoice`, or `MenuGroup`. + * + * ```jsx + * import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; + * import { more, arrowUp, arrowDown, trash } from '@wordpress/icons'; + * + * const MyDropdownMenu = () => ( + * + * { ( { onClose } ) => ( + * <> + * + * + * Move Up + * + * + * Move Down + * + * + * + * + * Remove + * + * + * + * ) } + * + * ); + * ``` + * + */ + function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { const { children, diff --git a/packages/components/src/dropdown-menu/stories/index.js b/packages/components/src/dropdown-menu/stories/index.js deleted file mode 100644 index 33477854721ee..0000000000000 --- a/packages/components/src/dropdown-menu/stories/index.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Internal dependencies - */ -import DropdownMenu from '../'; -import { MenuGroup, MenuItem } from '../../'; - -/** - * WordPress dependencies - */ -import { - menu, - arrowUp, - arrowDown, - chevronDown, - more, - trash, -} from '@wordpress/icons'; - -export default { - title: 'Components/DropdownMenu', - component: DropdownMenu, - argTypes: { - className: { control: { type: 'text' } }, - children: { control: { type: null } }, - disableOpenOnArrowDown: { control: { type: 'boolean' } }, - icon: { - options: [ 'menu', 'chevronDown', 'more' ], - mapping: { menu, chevronDown, more }, - control: { type: 'select' }, - }, - menuProps: { - control: { type: 'object' }, - }, - noIcons: { control: { type: 'boolean' } }, - popoverProps: { - control: { type: 'object' }, - }, - text: { control: { type: 'text' } }, - toggleProps: { - control: { type: 'object' }, - }, - }, - parameters: { - controls: { expanded: true }, - docs: { source: { state: 'open' } }, - }, -}; - -const Template = ( props ) => ( -
- -
-); - -export const Default = Template.bind( {} ); -Default.args = { - label: 'Select a direction.', - icon: menu, - controls: [ - { - title: 'First Menu Item Label', - icon: arrowUp, - // eslint-disable-next-line no-console - onClick: () => console.log( 'up!' ), - }, - { - title: 'Second Menu Item Label', - icon: arrowDown, - // eslint-disable-next-line no-console - onClick: () => console.log( 'down!' ), - }, - ], -}; - -export const WithChildren = Template.bind( {} ); -// Adding custom source because Storybook is not able to show the contents of -// the `children` prop correctly in the code snippet. -WithChildren.parameters = { - docs: { - source: { - code: ` - - - Move Up - - - Move Down - - - - - Remove - - -`, - language: 'jsx', - type: 'auto', - }, - }, -}; -WithChildren.args = { - label: 'Select a direction.', - icon: more, - children: ( { onClose } ) => ( - <> - - - Move Up - - - Move Down - - - - - Remove - - - - ), -}; From 8e44af5e4dc998b8b28993f9df9239107d7f070e Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:36:26 -0400 Subject: [PATCH 10/19] migrate storybook --- .../src/dropdown-menu/stories/index.tsx | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/components/src/dropdown-menu/stories/index.tsx diff --git a/packages/components/src/dropdown-menu/stories/index.tsx b/packages/components/src/dropdown-menu/stories/index.tsx new file mode 100644 index 0000000000000..a33cbbd8e0e51 --- /dev/null +++ b/packages/components/src/dropdown-menu/stories/index.tsx @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +/** + * Internal dependencies + */ +import DropdownMenu from '..'; +import { MenuGroup, MenuItem } from '../..'; + +/** + * WordPress dependencies + */ +import { menu, arrowUp, arrowDown, more, trash } from '@wordpress/icons'; + +const meta: ComponentMeta< typeof DropdownMenu > = { + title: 'Components/DropdownMenu', + component: DropdownMenu, + parameters: { + controls: { expanded: true }, + docs: { source: { state: 'open' } }, + }, +}; +export default meta; + +const Template: ComponentStory< typeof DropdownMenu > = ( props ) => ( +
+ +
+); + +export const Default = Template.bind( {} ); +Default.args = { + label: 'Select a direction.', + icon: menu, + controls: [ + { + title: 'First Menu Item Label', + icon: arrowUp, + // eslint-disable-next-line no-console + onClick: () => console.log( 'up!' ), + }, + { + title: 'Second Menu Item Label', + icon: arrowDown, + // eslint-disable-next-line no-console + onClick: () => console.log( 'down!' ), + }, + ], +}; + +export const WithChildren = Template.bind( {} ); +// Adding custom source because Storybook is not able to show the contents of +// the `children` prop correctly in the code snippet. +WithChildren.parameters = { + docs: { + source: { + code: ` + + + Move Up + + + Move Down + + + + + Remove + + +`, + language: 'jsx', + type: 'auto', + }, + }, +}; +WithChildren.args = { + label: 'Select a direction.', + icon: more, + children: ( { onClose } ) => ( + <> + + + Move Up + + + Move Down + + + + + Remove + + + + ), +}; From 406a07d58560370cb98329b46f7f253f0273b110 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:42:35 -0400 Subject: [PATCH 11/19] migrate unit tests --- .../src/dropdown-menu/test/{index.js => index.tsx} | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) rename packages/components/src/dropdown-menu/test/{index.js => index.tsx} (89%) diff --git a/packages/components/src/dropdown-menu/test/index.js b/packages/components/src/dropdown-menu/test/index.tsx similarity index 89% rename from packages/components/src/dropdown-menu/test/index.js rename to packages/components/src/dropdown-menu/test/index.tsx index b40ab218ccd9f..118e991812367 100644 --- a/packages/components/src/dropdown-menu/test/index.js +++ b/packages/components/src/dropdown-menu/test/index.tsx @@ -12,19 +12,19 @@ import { arrowLeft, arrowRight, arrowUp, arrowDown } from '@wordpress/icons'; /** * Internal dependencies */ -import DropdownMenu from '../'; -import { MenuItem } from '../../'; +import DropdownMenu from '..'; +import { MenuItem } from '../..'; describe( 'DropdownMenu', () => { it( 'should not render when neither controls nor children are assigned', () => { - render( ); + render( ); // The button toggle should not even be rendered expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument(); } ); it( 'should not render when controls are empty and children is not specified', () => { - render( ); + render( ); // The button toggle should not even be rendered expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument(); @@ -56,7 +56,7 @@ describe( 'DropdownMenu', () => { }, ]; - render( ); + render( ); // Move focus on the toggle button await user.tab(); @@ -78,6 +78,7 @@ describe( 'DropdownMenu', () => { render( } /> ); From 6f698bc72dba3ce67d05c7785787939a91a7f3a0 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Fri, 28 Apr 2023 12:02:54 -0400 Subject: [PATCH 12/19] update CHANGELOG --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6e46c3b70f495..90cc1258ee55b 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -21,6 +21,7 @@ - `NavigableContainer`: Convert to TypeScript ([#49377](https://github.com/WordPress/gutenberg/pull/49377)). - `ToolbarItem`: Convert to TypeScript ([#49190](https://github.com/WordPress/gutenberg/pull/49190)). +- `DropdownMenu`: Convert to TypeScript ([#50187](https://github.com/WordPress/gutenberg/pull/50187)). - Move rich-text related types to the rich-text package ([#49651](https://github.com/WordPress/gutenberg/pull/49651)). - `SlotFill`: simplified the implementation and removed unused code ([#50098](https://github.com/WordPress/gutenberg/pull/50098) and [#50133](https://github.com/WordPress/gutenberg/pull/50133)). From 1b6c0a210577fd2c5d7eb3f89bd14650b13aafba Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Mon, 1 May 2023 11:05:29 -0400 Subject: [PATCH 13/19] import `props from `NavigableMenu` --- packages/components/src/dropdown-menu/README.md | 3 +-- packages/components/src/dropdown-menu/types.ts | 13 ++----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index 69ecb954b6eb2..7f1f1ca5449c1 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -185,8 +185,7 @@ Use this object to modify props available for the `Button` component that are no - Required: No -// TODO: Update this when `NavigableMenu` is typed and `menuProps` is imported from there -#### `menuProps`: `Object` +#### `menuProps`: `NavigableContainerProps` Properties of `menuProps` object will be passed as props to the nested `NavigableMenu` component in the `renderContent` implementation of the `Dropdown` component used internally. Use this object to modify props available for the `NavigableMenu` component that are not already exposed in the `DropdownMenu` component, e.g.: the orientation of the menu set with `orientation` prop. diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts index 820d28160af50..4364d71e5178b 100644 --- a/packages/components/src/dropdown-menu/types.ts +++ b/packages/components/src/dropdown-menu/types.ts @@ -9,6 +9,7 @@ import type { ButtonAsButtonProps } from '../button/types'; import type { WordPressComponentProps } from '../ui/context'; import type { DropdownProps } from '../dropdown/types'; import type { Props as IconProps } from '../icon'; +import type { NavigableMenuProps } from '../navigable-container/types'; type DropdownOption = { /** @@ -60,15 +61,6 @@ type ToggleProps = Partial< as?: React.ElementType | keyof JSX.IntrinsicElements; }; -type MenuProps = { - onNavigate?: ( index: number, child: HTMLElement ) => void; - cycle?: boolean; - orientation?: 'horizontal' | 'vertical' | 'both'; - role?: string; - 'aria-label'?: string; - className?: string; -}; - export type DropdownMenuProps = { /** * The Dashicon icon slug to be shown in the collapsed menu button. @@ -111,8 +103,7 @@ export type DropdownMenuProps = { * component that are not already exposed in the `DropdownMenu` component, * e.g.: the orientation of the menu set with `orientation` prop. */ - // TODO: NavigableContainer is currently being typed. Just import these props from it when they're available. - menuProps?: Omit< MenuProps, 'children' >; + menuProps?: Omit< Partial< NavigableMenuProps >, 'children' >; /** * In some contexts, the arrow down key used to open the dropdown menu might * need to be disabled—for example when that key is used to perform another From 0aa94ecc659ae311cfeb2fb7f0460be6c812a8d9 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Tue, 9 May 2023 10:21:17 -0400 Subject: [PATCH 14/19] fix `toggleProps` type description in README --- packages/components/src/dropdown-menu/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index 7f1f1ca5449c1..7313ac4a3e3da 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -178,7 +178,7 @@ Use this object to modify props available for the `Popover` component that are n - Required: No -#### `toggleProps`: `Object` +#### `toggleProps`: `ToggleProps` Properties of `toggleProps` object will be passed as props to the nested `Button` component in the `renderToggle` implementation of the `Dropdown` component used internally. Use this object to modify props available for the `Button` component that are not already exposed in the `DropdownMenu` component, e.g.: the tooltip text displayed on hover set with `tooltip` prop. From ca31cf12eb38821a64ddd08c5a60411b3ca161e7 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Tue, 9 May 2023 12:44:53 -0400 Subject: [PATCH 15/19] allow for nested arrays of controls --- packages/components/src/dropdown-menu/README.md | 2 +- packages/components/src/dropdown-menu/index.tsx | 10 +++++----- packages/components/src/dropdown-menu/types.ts | 6 ++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index 7313ac4a3e3da..1315391229885 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -146,7 +146,7 @@ A human-readable label to present as accessibility text on the focused collapsed - Required: Yes -#### `controls:` `DropdownOption[]` +#### `controls:` `DropdownOption[] | DropdownOption[][]` An array of objects describing the options to be shown in the expanded menu. diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index eead1e193fcd1..c21cfa456eb3c 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -14,7 +14,7 @@ import { menu } from '@wordpress/icons'; import Button from '../button'; import Dropdown from '../dropdown'; import { NavigableMenu } from '../navigable-container'; -import type { DropdownMenuProps } from './types'; +import type { Controls, DropdownMenuProps, NormalizedControls } from './types'; function mergeProps< T extends { className?: string; [ key: string ]: unknown } @@ -139,15 +139,15 @@ function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { } // Normalize controls to nested array of objects (sets of controls) - let controlSets: NonNullable< typeof controls >[]; + let controlSets: NormalizedControls; if ( controls?.length ) { // @ts-expect-error The check below is needed because `DropdownMenus` // rendered by `ToolBarGroup` receive controls as a nested array. controlSets = controls; if ( ! Array.isArray( controlSets[ 0 ] ) ) { - // This is not ideal but was introduced to avoid runtime changes, - // see above comment. - controlSets = [ controls as unknown as typeof controls ]; + // This is not ideal, but at this point we know that `controls` is + // not a nested array, even if TypeScript doesn't. + controlSets = [ controls as Controls ]; } } diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts index 4364d71e5178b..a52e04ff0885b 100644 --- a/packages/components/src/dropdown-menu/types.ts +++ b/packages/components/src/dropdown-menu/types.ts @@ -44,6 +44,9 @@ type DropdownOption = { role?: HTMLElement[ 'role' ]; }; +export type Controls = DropdownOption[]; +export type NormalizedControls = DropdownOption[][]; + type DropdownCallbackProps = { isOpen: boolean; onToggle: () => void; @@ -139,6 +142,5 @@ export type DropdownMenuProps = { * * A valid DropdownMenu must specify a `controls` or `children` prop, or both. */ - - controls?: DropdownOption[]; + controls?: Controls | NormalizedControls; }; From 870f4e229b764be40e2c73449ff68695bff7f9a7 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Tue, 9 May 2023 13:01:10 -0400 Subject: [PATCH 16/19] restore custom icon control in storybook --- .../src/dropdown-menu/stories/index.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/components/src/dropdown-menu/stories/index.tsx b/packages/components/src/dropdown-menu/stories/index.tsx index a33cbbd8e0e51..97a51371d1ab8 100644 --- a/packages/components/src/dropdown-menu/stories/index.tsx +++ b/packages/components/src/dropdown-menu/stories/index.tsx @@ -11,7 +11,14 @@ import { MenuGroup, MenuItem } from '../..'; /** * WordPress dependencies */ -import { menu, arrowUp, arrowDown, more, trash } from '@wordpress/icons'; +import { + menu, + arrowUp, + arrowDown, + chevronDown, + more, + trash, +} from '@wordpress/icons'; const meta: ComponentMeta< typeof DropdownMenu > = { title: 'Components/DropdownMenu', @@ -20,6 +27,13 @@ const meta: ComponentMeta< typeof DropdownMenu > = { controls: { expanded: true }, docs: { source: { state: 'open' } }, }, + argTypes: { + icon: { + options: [ 'menu', 'chevronDown', 'more' ], + mapping: { menu, chevronDown, more }, + control: { type: 'select' }, + }, + }, }; export default meta; From c2a9ee75aed0d80867ad0ed753ec9819f967fbb0 Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Wed, 17 May 2023 06:45:39 -0400 Subject: [PATCH 17/19] update `controls` descriptions to mention nested arrays --- packages/components/src/dropdown-menu/README.md | 2 +- packages/components/src/dropdown-menu/types.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index 1315391229885..e1e4c7bf031b0 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -148,7 +148,7 @@ A human-readable label to present as accessibility text on the focused collapsed #### `controls:` `DropdownOption[] | DropdownOption[][]` -An array of objects describing the options to be shown in the expanded menu. +An array or nested array of objects describing the options to be shown in the expanded menu. Each object should include an `icon` [Dashicon](https://developer.wordpress.org/resource/dashicons/) slug string, a human-readable `title` string, `isDisabled` boolean flag and an `onClick` function callback to invoke when the option is selected. diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts index a52e04ff0885b..7b96d4a138b21 100644 --- a/packages/components/src/dropdown-menu/types.ts +++ b/packages/components/src/dropdown-menu/types.ts @@ -135,10 +135,10 @@ export type DropdownMenuProps = { */ children?: ( callbackProps: DropdownCallbackProps ) => ReactNode; /** - * An array of objects describing the options to be shown in the expanded - * menu. Each object should include an `icon` Dashicon slug string, a - * human-readable `title` string, `isDisabled` boolean flag, and an `onClick` - * function callback to invoke when the option is selected. + * An array or nested array of objects describing the options to be shown in + * the expanded menu. Each object should include an `icon` Dashicon slug + * string, a human-readable `title` string, `isDisabled` boolean flag, and + * an `onClick` function callback to invoke when the option is selected. * * A valid DropdownMenu must specify a `controls` or `children` prop, or both. */ From 0f925c178d1ed26c1dab4349710d9a4cd23ce72c Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Wed, 17 May 2023 06:50:20 -0400 Subject: [PATCH 18/19] Remove `Controls/NormalizedControls` abstraction --- packages/components/src/dropdown-menu/index.tsx | 6 +++--- packages/components/src/dropdown-menu/types.ts | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index c21cfa456eb3c..805bcd0661179 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -14,7 +14,7 @@ import { menu } from '@wordpress/icons'; import Button from '../button'; import Dropdown from '../dropdown'; import { NavigableMenu } from '../navigable-container'; -import type { Controls, DropdownMenuProps, NormalizedControls } from './types'; +import type { DropdownMenuProps, DropdownOption } from './types'; function mergeProps< T extends { className?: string; [ key: string ]: unknown } @@ -139,7 +139,7 @@ function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { } // Normalize controls to nested array of objects (sets of controls) - let controlSets: NormalizedControls; + let controlSets: DropdownOption[][]; if ( controls?.length ) { // @ts-expect-error The check below is needed because `DropdownMenus` // rendered by `ToolBarGroup` receive controls as a nested array. @@ -147,7 +147,7 @@ function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { if ( ! Array.isArray( controlSets[ 0 ] ) ) { // This is not ideal, but at this point we know that `controls` is // not a nested array, even if TypeScript doesn't. - controlSets = [ controls as Controls ]; + controlSets = [ controls as DropdownOption[] ]; } } diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts index 7b96d4a138b21..badfcb54d6072 100644 --- a/packages/components/src/dropdown-menu/types.ts +++ b/packages/components/src/dropdown-menu/types.ts @@ -11,7 +11,7 @@ import type { DropdownProps } from '../dropdown/types'; import type { Props as IconProps } from '../icon'; import type { NavigableMenuProps } from '../navigable-container/types'; -type DropdownOption = { +export type DropdownOption = { /** * The Dashicon icon slug to be shown for the option. */ @@ -44,9 +44,6 @@ type DropdownOption = { role?: HTMLElement[ 'role' ]; }; -export type Controls = DropdownOption[]; -export type NormalizedControls = DropdownOption[][]; - type DropdownCallbackProps = { isOpen: boolean; onToggle: () => void; @@ -142,5 +139,5 @@ export type DropdownMenuProps = { * * A valid DropdownMenu must specify a `controls` or `children` prop, or both. */ - controls?: Controls | NormalizedControls; + controls?: DropdownOption[] | DropdownOption[][]; }; From c3ac9705a4153c572ceb838801b2821fa7bcb92d Mon Sep 17 00:00:00 2001 From: chad1008 <13856531+chad1008@users.noreply.github.com> Date: Wed, 17 May 2023 07:37:47 -0400 Subject: [PATCH 19/19] move CHANGELOG update to correct section --- packages/components/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 90cc1258ee55b..d4cc445dd379d 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,6 +5,8 @@ ### Internal - `Modal`: Remove children container's unused class name ([#50655](https://github.com/WordPress/gutenberg/pull/50655)). +- `DropdownMenu`: Convert to TypeScript ([#50187](https://github.com/WordPress/gutenberg/pull/50187)). + ## 24.0.0 (2023-05-10) @@ -21,7 +23,6 @@ - `NavigableContainer`: Convert to TypeScript ([#49377](https://github.com/WordPress/gutenberg/pull/49377)). - `ToolbarItem`: Convert to TypeScript ([#49190](https://github.com/WordPress/gutenberg/pull/49190)). -- `DropdownMenu`: Convert to TypeScript ([#50187](https://github.com/WordPress/gutenberg/pull/50187)). - Move rich-text related types to the rich-text package ([#49651](https://github.com/WordPress/gutenberg/pull/49651)). - `SlotFill`: simplified the implementation and removed unused code ([#50098](https://github.com/WordPress/gutenberg/pull/50098) and [#50133](https://github.com/WordPress/gutenberg/pull/50133)).