Skip to content

Commit

Permalink
ConfirmDialog: ts unit test storybook (#54954)
Browse files Browse the repository at this point in the history
* 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 <marco.ciampo@gmail.com>
  • Loading branch information
margolisj and ciampo authored Oct 7, 2023
1 parent d16cf6b commit bd48f41
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 53 deletions.
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/confirm-dialog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
92 changes: 79 additions & 13 deletions packages/components/src/confirm-dialog/component.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import type { ForwardedRef, KeyboardEvent } from 'react';

/**
* WordPress dependencies
*/
Expand All @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 (
* <ConfirmDialog onConfirm={ () => console.debug( ' Confirmed! ' ) }>
* Are you sure? <strong>This action cannot be undone!</strong>
* </ConfirmDialog>
* );
* }
* ```
*
*
* 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 (
* <ConfirmDialog
* isOpen={ isOpen }
* onConfirm={ handleConfirm }
* onCancel={ handleCancel }
* >
* Are you sure? <strong>This action cannot be undone!</strong>
* </ConfirmDialog>
* );
* }
* ```
*/
export const ConfirmDialog = contextConnect(
UnconnectedConfirmDialog,
'ConfirmDialog'
);
export default ConfirmDialog;
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import type { Meta, StoryFn } from '@storybook/react';

/**
* WordPress dependencies
*/
Expand All @@ -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 );
};

Expand All @@ -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('');
Expand Down Expand Up @@ -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,
Expand All @@ -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!',
};
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
} );
Expand Down
44 changes: 32 additions & 12 deletions packages/components/src/confirm-dialog/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit bd48f41

Please sign in to comment.