From 777af7cc8c6ffc25b49a232778a74322ed7c16f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Walb=C3=B8=20Johnsg=C3=A5rd?= Date: Tue, 14 Jun 2022 20:21:55 +0200 Subject: [PATCH 1/5] FormToggle: Covert component to TypeScript --- packages/components/src/form-toggle/README.md | 13 +-- packages/components/src/form-toggle/index.js | 38 ------ packages/components/src/form-toggle/index.tsx | 69 +++++++++++ .../src/form-toggle/stories/index.js | 28 ----- .../src/form-toggle/stories/index.tsx | 52 +++++++++ .../test/__snapshots__/index.tsx.snap | 54 +++++++++ .../components/src/form-toggle/test/index.js | 74 ------------ .../components/src/form-toggle/test/index.tsx | 109 ++++++++++++++++++ packages/components/src/form-toggle/types.ts | 22 ++++ packages/components/tsconfig.json | 1 + 10 files changed, 312 insertions(+), 148 deletions(-) delete mode 100644 packages/components/src/form-toggle/index.js create mode 100644 packages/components/src/form-toggle/index.tsx delete mode 100644 packages/components/src/form-toggle/stories/index.js create mode 100644 packages/components/src/form-toggle/stories/index.tsx create mode 100644 packages/components/src/form-toggle/test/__snapshots__/index.tsx.snap delete mode 100644 packages/components/src/form-toggle/test/index.js create mode 100644 packages/components/src/form-toggle/test/index.tsx create mode 100644 packages/components/src/form-toggle/types.ts diff --git a/packages/components/src/form-toggle/README.md b/packages/components/src/form-toggle/README.md index abfa4766c6011f..6f319943ca3bff 100644 --- a/packages/components/src/form-toggle/README.md +++ b/packages/components/src/form-toggle/README.md @@ -63,7 +63,7 @@ const MyFormToggle = () => { setChecked( ( state ) => ! state ) } - /> + />; }; ``` @@ -71,26 +71,23 @@ const MyFormToggle = () => { The component accepts the following props: -#### checked +#### `checked`: `boolean` If checked is true the toggle will be checked. If checked is false the toggle will be unchecked. If no value is passed the toggle will be unchecked. -- Type: `Boolean` - Required: No -#### disabled +#### `disabled`: `boolean` If disabled is true the toggle will be disabled and apply the appropriate styles. -- Type: `Boolean` - Required: No -#### onChange +#### `onChange`: `( event: ChangeEvent ) => void` -A function that receives the checked state (boolean) as input. +A callback function invoked when the toggle is clicked. -- Type: `function` - Required: Yes ## Related components diff --git a/packages/components/src/form-toggle/index.js b/packages/components/src/form-toggle/index.js deleted file mode 100644 index 8714958ae48c4b..00000000000000 --- a/packages/components/src/form-toggle/index.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -export const noop = () => {}; - -function FormToggle( { - className, - checked, - id, - disabled, - onChange = noop, - ...props -} ) { - const wrapperClasses = classnames( 'components-form-toggle', className, { - 'is-checked': checked, - 'is-disabled': disabled, - } ); - - return ( - - - - - - ); -} - -export default FormToggle; diff --git a/packages/components/src/form-toggle/index.tsx b/packages/components/src/form-toggle/index.tsx new file mode 100644 index 00000000000000..3e5251900e663f --- /dev/null +++ b/packages/components/src/form-toggle/index.tsx @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import type { FormToggleProps } from './types'; +import type { WordPressComponentProps } from '../ui/context'; + +export const noop = () => {}; + +/** + * FormToggle switches a single setting on or off. + * + * ```jsx + * import { FormToggle } from '@wordpress/components'; + * import { useState } from '@wordpress/element'; + * + * const MyFormToggle = () => { + * const [ isChecked, setChecked ] = useState( true ); + * + * setChecked( ( state ) => ! state ) } + * />; + * }; + * ``` + */ +export function FormToggle( + // ref is omitted until we have `WordPressComponentPropsWithoutRef` or add + // ref forwarding to FormToggle. + props: Omit< + WordPressComponentProps< FormToggleProps, 'input', false >, + 'ref' + > +) { + const { + className, + checked, + id, + disabled, + onChange = noop, + ...additionalProps + } = props; + const wrapperClasses = classnames( 'components-form-toggle', className, { + 'is-checked': checked, + 'is-disabled': disabled, + } ); + + return ( + + + + + + ); +} + +export default FormToggle; diff --git a/packages/components/src/form-toggle/stories/index.js b/packages/components/src/form-toggle/stories/index.js deleted file mode 100644 index 5f4f86a82034d6..00000000000000 --- a/packages/components/src/form-toggle/stories/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import FormToggle from '../'; - -export default { title: 'Components/FormToggle', component: FormToggle }; - -const FormToggleWithState = ( { checked, ...props } ) => { - const [ isChecked, setChecked ] = useState( checked ); - return ( - { - setChecked( ! isChecked ); - } } - /> - ); -}; - -export const _default = () => { - return ; -}; diff --git a/packages/components/src/form-toggle/stories/index.tsx b/packages/components/src/form-toggle/stories/index.tsx new file mode 100644 index 00000000000000..eba2bf4c8580c8 --- /dev/null +++ b/packages/components/src/form-toggle/stories/index.tsx @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { FormToggle } from '..'; + +const meta: ComponentMeta< typeof FormToggle > = { + component: FormToggle, + title: 'Components/FormToggle', + argTypes: { + onChange: { + action: 'onChange', + }, + }, + parameters: { + controls: { + expanded: true, + }, + docs: { source: { state: 'open' } }, + }, +}; +export default meta; + +const Template: ComponentStory< typeof FormToggle > = ( { + onChange, + ...args +} ) => { + const [ isChecked, setChecked ] = useState( true ); + + return ( + { + setChecked( ( state ) => ! state ); + onChange( e ); + } } + /> + ); +}; + +export const Default: ComponentStory< typeof FormToggle > = Template.bind( {} ); +Default.args = {}; diff --git a/packages/components/src/form-toggle/test/__snapshots__/index.tsx.snap b/packages/components/src/form-toggle/test/__snapshots__/index.tsx.snap new file mode 100644 index 00000000000000..67617127fcebde --- /dev/null +++ b/packages/components/src/form-toggle/test/__snapshots__/index.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FormToggle basic rendering should render a span element with an unchecked checkbox 1`] = ` + + + + + +`; + +exports[`FormToggle basic rendering should render an id prop for the input checkbox 1`] = ` +Snapshot Diff: +- First value ++ Second value + +@@ -2,10 +2,11 @@ + + + +`; + +exports[`FormToggle basic rendering should render with an additional className 1`] = ` +Snapshot Diff: +- First value ++ Second value + +@@ -1,8 +1,8 @@ +
+ + +`; diff --git a/packages/components/src/form-toggle/test/index.js b/packages/components/src/form-toggle/test/index.js deleted file mode 100644 index 9f9c3b84f27644..00000000000000 --- a/packages/components/src/form-toggle/test/index.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * Internal dependencies - */ -import FormToggle, { noop } from '../'; - -describe( 'FormToggle', () => { - describe( 'basic rendering', () => { - it( 'should render a span element with an unchecked checkbox', () => { - const formToggle = shallow( ); - expect( formToggle.hasClass( 'components-form-toggle' ) ).toBe( - true - ); - expect( formToggle.hasClass( 'is-checked' ) ).toBe( false ); - expect( formToggle.type() ).toBe( 'span' ); - } ); - - it( 'should render a checked checkbox and change the accessibility text to On when providing checked prop', () => { - const formToggle = shallow( ); - expect( formToggle.hasClass( 'is-checked' ) ).toBe( true ); - expect( - formToggle - .find( '.components-form-toggle__input' ) - .prop( 'checked' ) - ).toBe( true ); - } ); - - it( 'should render with an additional className', () => { - const formToggle = shallow( ); - expect( formToggle.hasClass( 'testing' ) ).toBe( true ); - } ); - - it( 'should render an id prop for the input checkbox', () => { - // Disabled because of our rule restricting literal IDs, preferring - // `withInstanceId`. In this case, it's fine to use literal IDs. - // eslint-disable-next-line no-restricted-syntax - const formToggle = shallow( ); - expect( - formToggle.find( '.components-form-toggle__input' ).prop( 'id' ) - ).toBe( 'test' ); - } ); - - it( 'should render a checkbox with a noop onChange', () => { - const formToggle = shallow( ); - const checkBox = formToggle - .prop( 'children' ) - .find( - ( child ) => - 'input' === child.type && - 'checkbox' === child.props.type - ); - expect( checkBox.props.onChange ).toBe( noop ); - } ); - - it( 'should render a checkbox with a user-provided onChange', () => { - const testFunction = ( event ) => event; - const formToggle = shallow( - - ); - const checkBox = formToggle - .prop( 'children' ) - .find( - ( child ) => - 'input' === child.type && - 'checkbox' === child.props.type - ); - expect( checkBox.props.onChange ).toBe( testFunction ); - } ); - } ); -} ); diff --git a/packages/components/src/form-toggle/test/index.tsx b/packages/components/src/form-toggle/test/index.tsx new file mode 100644 index 00000000000000..b05ef94cb42a98 --- /dev/null +++ b/packages/components/src/form-toggle/test/index.tsx @@ -0,0 +1,109 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import FormToggle, { noop } from '..'; +import type { FormToggleProps } from '../types'; + +const getInput = () => screen.getByRole( 'checkbox' ) as HTMLInputElement; + +const ControlledFormToggle = ( { onChange }: FormToggleProps ) => { + const [ isChecked, setChecked ] = useState( false ); + return ( + { + setChecked( ( state ) => ! state ); + onChange( value ); + } } + /> + ); +}; + +describe( 'FormToggle', () => { + describe( 'basic rendering', () => { + it( 'should render', () => { + render( ); + expect( getInput() ).not.toBeNull(); + } ); + + it( 'should render a span element with an unchecked checkbox', () => { + const { container } = render( ); + + expect( getInput() ).toHaveProperty( 'checked', false ); + expect( container.firstChild ).toMatchSnapshot(); + } ); + + it( 'should render a checked checkbox and change the accessibility text to On when providing checked prop', () => { + render( ); + + expect( getInput() ).toHaveProperty( 'checked', true ); + } ); + + it( 'should render with an additional className', () => { + const { container: containerDefault } = render( + + ); + + const { container: containerWithClassName } = render( + + ); + + // Expect the diff snapshot to be mostly about the className. + expect( containerDefault ).toMatchDiffSnapshot( + containerWithClassName + ); + } ); + + it( 'should render an id prop for the input checkbox', () => { + const { container: containerDefault } = render( + + ); + + const { container: containerWithID } = render( + // Disabled because of our rule restricting literal IDs, preferring + // `withInstanceId`. In this case, it's fine to use literal IDs. + // eslint-disable-next-line no-restricted-syntax + + ); + + // Expect the diff snapshot to be mostly about the ID. + expect( containerDefault ).toMatchDiffSnapshot( containerWithID ); + } ); + } ); + + describe( 'Value', () => { + it( 'should flip the checked property when clicked', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); + + let state = false; + const setState = jest.fn( () => ( state = ! state ) ); + + render( ); + + const input = getInput(); + + await user.click( input ); + expect( input ).toHaveProperty( 'checked', true ); + expect( state ).toBe( true ); + + await user.click( input ); + expect( input ).toHaveProperty( 'checked', false ); + expect( state ).toBe( false ); + + expect( setState ).toHaveBeenCalledTimes( 2 ); + } ); + } ); +} ); diff --git a/packages/components/src/form-toggle/types.ts b/packages/components/src/form-toggle/types.ts new file mode 100644 index 00000000000000..157d75b1b0cdd4 --- /dev/null +++ b/packages/components/src/form-toggle/types.ts @@ -0,0 +1,22 @@ +/** + * External dependencies + */ +import type { ChangeEvent } from 'react'; + +export type FormToggleProps = { + /** + * If checked is true the toggle will be checked. If checked is false the + * toggle will be unchecked. If no value is passed the toggle will be + * unchecked. + */ + checked?: boolean; + /** + * If disabled is true the toggle will be disabled and apply the appropriate + * styles. + */ + disabled?: boolean; + /** + * A callback function invoked when the toggle is clicked. + */ + onChange: ( event: ChangeEvent< HTMLInputElement > ) => void; +}; diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index 060575f52426d7..a930377e14fe6e 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -57,6 +57,7 @@ "src/external-link/**/*", "src/flex/**/*", "src/form-group/**/*", + "src/form-toggle/**/*", "src/form-token-field/**/*", "src/grid/**/*", "src/h-stack/**/*", From 6c82fad90169b8e3d127cc631f71454a25dfffa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Walb=C3=B8=20Johnsg=C3=A5rd?= Date: Tue, 14 Jun 2022 20:33:28 +0200 Subject: [PATCH 2/5] Update CHANGELOG.md --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 353199871a17e9..596b8abcb02ce8 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -26,6 +26,7 @@ - `Dropdown`: Make sure cleanup (closing the dropdown) only runs when the menu has actually been opened. - Enhance the TypeScript migration guidelines ([#41669](https://github.com/WordPress/gutenberg/pull/41669)). - `ExternalLink`: Convert to TypeScript ([#41681](https://github.com/WordPress/gutenberg/pull/41681)). +- `FormToggle`: Convert to TypeScript ([#41729](https://github.com/WordPress/gutenberg/pull/41729)). ## 19.12.0 (2022-06-01) From 20052651c961eab3d687d6b547a87436a89bcb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Walb=C3=B8=20Johnsg=C3=A5rd?= Date: Tue, 21 Jun 2022 16:51:33 +0200 Subject: [PATCH 3/5] Update CHANGELOG.md --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index e496176e93595c..6b25f9961189ce 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,7 @@ - `Spinner`: Convert to TypeScript and update storybook ([#41540](https://github.com/WordPress/gutenberg/pull/41540/)). - `InputControl`: Add tests and update to use `@testing-library/user-event` ([#41421](https://github.com/WordPress/gutenberg/pull/41421)). +- `FormToggle`: Convert to TypeScript ([#41729](https://github.com/WordPress/gutenberg/pull/41729)). - `ColorIndicator`: Convert to TypeScript ([#41587](https://github.com/WordPress/gutenberg/pull/41587)). - `AlignmentMatrixControl`: Refactor away from `_.flattenDeep()` in utils ([#41814](https://github.com/WordPress/gutenberg/pull/41814/)). From ed765164788e1c6e748f0f71942b4bdbfa9123e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Walb=C3=B8=20Johnsg=C3=A5rd?= Date: Tue, 21 Jun 2022 16:52:54 +0200 Subject: [PATCH 4/5] Add return --- packages/components/src/form-toggle/README.md | 10 ++++++---- packages/components/src/form-toggle/index.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/components/src/form-toggle/README.md b/packages/components/src/form-toggle/README.md index 6f319943ca3bff..941e431654e50b 100644 --- a/packages/components/src/form-toggle/README.md +++ b/packages/components/src/form-toggle/README.md @@ -60,10 +60,12 @@ import { useState } from '@wordpress/element'; const MyFormToggle = () => { const [ isChecked, setChecked ] = useState( true ); - setChecked( ( state ) => ! state ) } - />; + return ( + setChecked( ( state ) => ! state ) } + /> + ); }; ``` diff --git a/packages/components/src/form-toggle/index.tsx b/packages/components/src/form-toggle/index.tsx index 3e5251900e663f..d51a3ee571934d 100644 --- a/packages/components/src/form-toggle/index.tsx +++ b/packages/components/src/form-toggle/index.tsx @@ -21,10 +21,12 @@ export const noop = () => {}; * const MyFormToggle = () => { * const [ isChecked, setChecked ] = useState( true ); * - * setChecked( ( state ) => ! state ) } - * />; + * return ( + * setChecked( ( state ) => ! state ) } + * /> + * ); * }; * ``` */ From cfa24023e85faef5da67294436314e435ccf37a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Walb=C3=B8=20Johnsg=C3=A5rd?= Date: Tue, 21 Jun 2022 17:01:34 +0200 Subject: [PATCH 5/5] Update tests --- .../components/src/form-toggle/test/index.tsx | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/components/src/form-toggle/test/index.tsx b/packages/components/src/form-toggle/test/index.tsx index b05ef94cb42a98..38b84b86c226b7 100644 --- a/packages/components/src/form-toggle/test/index.tsx +++ b/packages/components/src/form-toggle/test/index.tsx @@ -32,22 +32,17 @@ const ControlledFormToggle = ( { onChange }: FormToggleProps ) => { describe( 'FormToggle', () => { describe( 'basic rendering', () => { - it( 'should render', () => { - render( ); - expect( getInput() ).not.toBeNull(); - } ); - it( 'should render a span element with an unchecked checkbox', () => { const { container } = render( ); - expect( getInput() ).toHaveProperty( 'checked', false ); + expect( getInput() ).not.toBeChecked(); expect( container.firstChild ).toMatchSnapshot(); } ); - it( 'should render a checked checkbox and change the accessibility text to On when providing checked prop', () => { + it( 'should render a checked checkbox when providing checked prop', () => { render( ); - expect( getInput() ).toHaveProperty( 'checked', true ); + expect( getInput() ).toBeChecked(); } ); it( 'should render with an additional className', () => { @@ -88,22 +83,20 @@ describe( 'FormToggle', () => { advanceTimers: jest.advanceTimersByTime, } ); - let state = false; - const setState = jest.fn( () => ( state = ! state ) ); - - render( ); + const onChange = jest.fn(); + render( ); const input = getInput(); await user.click( input ); - expect( input ).toHaveProperty( 'checked', true ); - expect( state ).toBe( true ); + expect( onChange.mock.calls[ 0 ][ 0 ].target ).toBeInTheDocument(); + expect( input ).toBeChecked(); await user.click( input ); - expect( input ).toHaveProperty( 'checked', false ); - expect( state ).toBe( false ); + expect( onChange.mock.calls[ 1 ][ 0 ].target ).toBeInTheDocument(); + expect( input ).not.toBeChecked(); - expect( setState ).toHaveBeenCalledTimes( 2 ); + expect( onChange ).toHaveBeenCalledTimes( 2 ); } ); } ); } );