From bd48f4146cf554cd6f2853dfc9205d19bfe2e80b Mon Sep 17 00:00:00 2001 From: margolisj <1588194+margolisj@users.noreply.github.com> Date: Sat, 7 Oct 2023 08:56:17 -0400 Subject: [PATCH] ConfirmDialog: ts unit test storybook (#54954) * Swaps file type. * Can't remember what point this served. * Attempting w/ functional component * Updates types wrt to PR notes, js -> TSX for storybook, fixes null check in tests. * Adds JSDocs to types and long form to component. * Adds changelog. * Skips null check in test * Exports and imports the correct one and edits the configs. * New changelog entry * Update packages/components/src/confirm-dialog/stories/index.story.tsx --------- Co-authored-by: Marco Ciampini --- packages/components/CHANGELOG.md | 4 + .../components/src/confirm-dialog/README.md | 2 +- .../src/confirm-dialog/component.tsx | 92 ++++++++++++++++--- .../{index.story.js => index.story.tsx} | 50 +++++----- .../test/{index.js => index.tsx} | 6 +- .../components/src/confirm-dialog/types.ts | 44 ++++++--- 6 files changed, 145 insertions(+), 53 deletions(-) rename packages/components/src/confirm-dialog/stories/{index.story.js => index.story.tsx} (75%) rename packages/components/src/confirm-dialog/test/{index.js => index.tsx} (98%) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2df99e7413ffa6..6b3734dfdabcdf 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,10 @@ - `Modal`: fix closing when contained iframe is focused ([#51602](https://github.com/WordPress/gutenberg/pull/51602)). +### Internal + +- `ConfirmDialog`: Migrate to TypeScript. ([#54954](https://github.com/WordPress/gutenberg/pull/54954)). + ## 25.9.0 (2023-10-05) ### Enhancements diff --git a/packages/components/src/confirm-dialog/README.md b/packages/components/src/confirm-dialog/README.md index 86d38bccdec3c6..4b0f37f5d35b39 100644 --- a/packages/components/src/confirm-dialog/README.md +++ b/packages/components/src/confirm-dialog/README.md @@ -137,4 +137,4 @@ The optional custom text to display as the confirmation button's label - Required: No - Default: "Cancel" -The optional custom text to display as the cancelation button's label +The optional custom text to display as the cancellation button's label diff --git a/packages/components/src/confirm-dialog/component.tsx b/packages/components/src/confirm-dialog/component.tsx index 4a8efd06e139c7..750e7030de13cd 100644 --- a/packages/components/src/confirm-dialog/component.tsx +++ b/packages/components/src/confirm-dialog/component.tsx @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import type { ForwardedRef, KeyboardEvent } from 'react'; - /** * WordPress dependencies */ @@ -13,7 +8,7 @@ import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; * Internal dependencies */ import Modal from '../modal'; -import type { OwnProps, DialogInputEvent } from './types'; +import type { ConfirmDialogProps, DialogInputEvent } from './types'; import type { WordPressComponentProps } from '../context'; import { useContextSystem, contextConnect } from '../context'; import { Flex } from '../flex'; @@ -23,10 +18,10 @@ import { VStack } from '../v-stack'; import * as styles from './styles'; import { useCx } from '../utils/hooks/use-cx'; -function ConfirmDialog( - props: WordPressComponentProps< OwnProps, 'div', false >, - forwardedRef: ForwardedRef< any > -) { +const UnconnectedConfirmDialog = ( + props: WordPressComponentProps< ConfirmDialogProps, 'div', false >, + forwardedRef: React.ForwardedRef< any > +) => { const { isOpen: isOpenProp, onConfirm, @@ -67,7 +62,7 @@ function ConfirmDialog( ); const handleEnter = useCallback( - ( event: KeyboardEvent< HTMLDivElement > ) => { + ( event: React.KeyboardEvent< HTMLDivElement > ) => { // Avoid triggering the 'confirm' action when a button is focused, // as this can cause a double submission. const isConfirmOrCancelButton = @@ -120,6 +115,77 @@ function ConfirmDialog( ) } ); -} +}; -export default contextConnect( ConfirmDialog, 'ConfirmDialog' ); +/** + * `ConfirmDialog` is built of top of [`Modal`](/packages/components/src/modal/README.md) + * and displays a confirmation dialog, with _confirm_ and _cancel_ buttons. + * The dialog is confirmed by clicking the _confirm_ button or by pressing the `Enter` key. + * It is cancelled (closed) by clicking the _cancel_ button, by pressing the `ESC` key, or by + * clicking outside the dialog focus (i.e, the overlay). + * + * `ConfirmDialog` has two main implicit modes: controlled and uncontrolled. + * + * UnControlled: + * + * Allows the component to be used standalone, just by declaring it as part of another React's component render method: + * - It will be automatically open (displayed) upon mounting; + * - It will be automatically closed when clicking the _cancel_ button, by pressing the `ESC` key, or by clicking outside the dialog focus (i.e, the overlay); + * - `onCancel` is not mandatory but can be passed. Even if passed, the dialog will still be able to close itself. + * + * Activating this mode is as simple as omitting the `isOpen` prop. The only mandatory prop, in this case, is the `onConfirm` callback. The message is passed as the `children`. You can pass any JSX you'd like, which allows to further format the message or include sub-component if you'd like: + * + * ```jsx + * import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components'; + * + * function Example() { + * return ( + * console.debug( ' Confirmed! ' ) }> + * Are you sure? This action cannot be undone! + * + * ); + * } + * ``` + * + * + * Controlled mode: + * Let the parent component control when the dialog is open/closed. It's activated when a + * boolean value is passed to `isOpen`: + * - It will not be automatically closed. You need to let it know when to open/close by updating the value of the `isOpen` prop; + * - Both `onConfirm` and the `onCancel` callbacks are mandatory props in this mode; + * - You'll want to update the state that controls `isOpen` by updating it from the `onCancel` and `onConfirm` callbacks. + * + *```jsx + * import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components'; + * import { useState } from '@wordpress/element'; + * + * function Example() { + * const [ isOpen, setIsOpen ] = useState( true ); + * + * const handleConfirm = () => { + * console.debug( 'Confirmed!' ); + * setIsOpen( false ); + * }; + * + * const handleCancel = () => { + * console.debug( 'Cancelled!' ); + * setIsOpen( false ); + * }; + * + * return ( + * + * Are you sure? This action cannot be undone! + * + * ); + * } + * ``` + */ +export const ConfirmDialog = contextConnect( + UnconnectedConfirmDialog, + 'ConfirmDialog' +); +export default ConfirmDialog; diff --git a/packages/components/src/confirm-dialog/stories/index.story.js b/packages/components/src/confirm-dialog/stories/index.story.tsx similarity index 75% rename from packages/components/src/confirm-dialog/stories/index.story.js rename to packages/components/src/confirm-dialog/stories/index.story.tsx index ea561ff297c436..85636c0ddc81ed 100644 --- a/packages/components/src/confirm-dialog/stories/index.story.js +++ b/packages/components/src/confirm-dialog/stories/index.story.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import type { Meta, StoryFn } from '@storybook/react'; + /** * WordPress dependencies */ @@ -7,47 +12,41 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import Button from '../../button'; -import { ConfirmDialog } from '..'; +import { ConfirmDialog } from '../component'; -const meta = { +const meta: Meta< typeof ConfirmDialog > = { component: ConfirmDialog, title: 'Components (Experimental)/ConfirmDialog', argTypes: { - children: { - control: { type: 'text' }, - }, - confirmButtonText: { - control: { type: 'text' }, - }, - cancelButtonText: { - control: { type: 'text' }, - }, isOpen: { control: { type: null }, }, - onConfirm: { action: 'onConfirm' }, - onCancel: { action: 'onCancel' }, - }, - args: { - children: 'Would you like to privately publish the post now?', }, parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { + expanded: true, + }, docs: { canvas: { sourceState: 'shown' } }, }, }; export default meta; -const Template = ( { onConfirm, onCancel, ...args } ) => { +const Template: StoryFn< typeof ConfirmDialog > = ( { + onConfirm, + onCancel, + ...args +} ) => { const [ isOpen, setIsOpen ] = useState( false ); - const handleConfirm = ( ...confirmArgs ) => { - onConfirm( ...confirmArgs ); + const handleConfirm: typeof onConfirm = ( confirmArgs ) => { + onConfirm( confirmArgs ); setIsOpen( false ); }; - const handleCancel = ( ...cancelArgs ) => { - onCancel( ...cancelArgs ); + const handleCancel: typeof onCancel = ( cancelArgs ) => { + onCancel?.( cancelArgs ); setIsOpen( false ); }; @@ -70,7 +69,7 @@ const Template = ( { onConfirm, onCancel, ...args } ) => { }; // Simplest usage: just declare the component with the required `onConfirm` prop. Note: the `onCancel` prop is optional here, unless you'd like to render the component in Controlled mode (see below) -export const _default = Template.bind( {} ); +export const Default = Template.bind( {} ); const _defaultSnippet = `() => { const [ isOpen, setIsOpen ] = useState( false ); const [ confirmVal, setConfirmVal ] = useState(''); @@ -103,8 +102,10 @@ const _defaultSnippet = `() => { ); };`; -_default.args = {}; -_default.parameters = { +Default.args = { + children: 'Would you like to privately publish the post now?', +}; +Default.parameters = { docs: { source: { code: _defaultSnippet, @@ -117,6 +118,7 @@ _default.parameters = { // To customize button text, pass the `cancelButtonText` and/or `confirmButtonText` props. export const WithCustomButtonLabels = Template.bind( {} ); WithCustomButtonLabels.args = { + ...Default.args, cancelButtonText: 'No thanks', confirmButtonText: 'Yes please!', }; diff --git a/packages/components/src/confirm-dialog/test/index.js b/packages/components/src/confirm-dialog/test/index.tsx similarity index 98% rename from packages/components/src/confirm-dialog/test/index.js rename to packages/components/src/confirm-dialog/test/index.tsx index adf19b292898f8..27e1af66ce7429 100644 --- a/packages/components/src/confirm-dialog/test/index.js +++ b/packages/components/src/confirm-dialog/test/index.tsx @@ -113,7 +113,7 @@ describe( 'Confirm', () => { expect( onCancel ).toHaveBeenCalled(); } ); - it( 'should be dismissable even if an `onCancel` callback is not provided', async () => { + it( 'should be dismissible even if an `onCancel` callback is not provided', async () => { const user = userEvent.setup(); render( @@ -144,7 +144,7 @@ describe( 'Confirm', () => { // Disable reason: Semantic queries can’t reach the overlay. // eslint-disable-next-line testing-library/no-node-access - await user.click( confirmDialog.parentElement ); + await user.click( confirmDialog.parentElement! ); expect( confirmDialog ).not.toBeInTheDocument(); expect( onCancel ).toHaveBeenCalled(); @@ -325,7 +325,7 @@ describe( 'Confirm', () => { // Disable reason: Semantic queries can’t reach the overlay. // eslint-disable-next-line testing-library/no-node-access - await user.click( confirmDialog.parentElement ); + await user.click( confirmDialog.parentElement! ); expect( onCancel ).toHaveBeenCalled(); } ); diff --git a/packages/components/src/confirm-dialog/types.ts b/packages/components/src/confirm-dialog/types.ts index 72fef59dc20094..b456b9bc4df196 100644 --- a/packages/components/src/confirm-dialog/types.ts +++ b/packages/components/src/confirm-dialog/types.ts @@ -13,21 +13,41 @@ export type DialogInputEvent = | KeyboardEvent< HTMLDivElement > | MouseEvent< HTMLButtonElement >; -type BaseProps = { +export type ConfirmDialogProps = { + /** + * The actual message for the dialog. It's passed as children and any valid `ReactNode` is accepted. + */ children: ReactNode; + /** + * The callback that's called when the user confirms. + * A confirmation can happen when the `OK` button is clicked or when `Enter` is pressed. + */ onConfirm: ( event: DialogInputEvent ) => void; + /** + * The optional custom text to display as the confirmation button's label. + */ confirmButtonText?: string; + /** + * The optional custom text to display as the cancellation button's label. + */ cancelButtonText?: string; -}; - -type ControlledProps = BaseProps & { - onCancel: ( event: DialogInputEvent ) => void; - isOpen: boolean; -}; - -type UncontrolledProps = BaseProps & { + /** + * The callback that's called when the user cancels. A cancellation can happen + * when the `Cancel` button is clicked, when the `ESC` key is pressed, or when + * a click outside of the dialog focus is detected (i.e. in the overlay). + * + * It's not required if `isOpen` is not set (uncontrolled mode), as the component + * will take care of closing itself, but you can still pass a callback if something + * must be done upon cancelling (the component will still close itself in this case). + * + * If `isOpen` is set (controlled mode), then it's required, and you need to set + * the state that defines `isOpen` to `false` as part of this callback if you want the + * dialog to close when the user cancels. + */ onCancel?: ( event: DialogInputEvent ) => void; - isOpen?: never; + /** + * Defines if the dialog is open (displayed) or closed (not rendered/displayed). + * It also implicitly toggles the controlled mode if set or the uncontrolled mode if it's not set. + */ + isOpen?: boolean; }; - -export type OwnProps = ControlledProps | UncontrolledProps;