From 89049f581fb1cbe9a5f843097b42def0ffc64200 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 25 Oct 2022 10:48:19 +0200 Subject: [PATCH 1/9] IsolatedEventContainer: ensure there is type for children --- .../src/isolated-event-container/index.js | 28 ---------------- .../src/isolated-event-container/index.tsx | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+), 28 deletions(-) delete mode 100644 packages/components/src/isolated-event-container/index.js create mode 100644 packages/components/src/isolated-event-container/index.tsx diff --git a/packages/components/src/isolated-event-container/index.js b/packages/components/src/isolated-event-container/index.js deleted file mode 100644 index 5b24d36d0b1ac..0000000000000 --- a/packages/components/src/isolated-event-container/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * WordPress dependencies - */ -import { forwardRef } from '@wordpress/element'; -import deprecated from '@wordpress/deprecated'; - -/** - * @type {import('react').MouseEventHandler} - */ -function stopPropagation( event ) { - event.stopPropagation(); -} - -export default forwardRef( ( { children, ...props }, ref ) => { - deprecated( 'wp.components.IsolatedEventContainer', { - since: '5.7', - } ); - - // Disable reason: this stops certain events from propagating outside of the component. - // - onMouseDown is disabled as this can cause interactions with other DOM elements. - /* eslint-disable jsx-a11y/no-static-element-interactions */ - return ( -
- { children } -
- ); - /* eslint-enable jsx-a11y/no-static-element-interactions */ -} ); diff --git a/packages/components/src/isolated-event-container/index.tsx b/packages/components/src/isolated-event-container/index.tsx new file mode 100644 index 0000000000000..267efd945a31e --- /dev/null +++ b/packages/components/src/isolated-event-container/index.tsx @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import type { ComponentPropsWithoutRef, MouseEvent } from 'react'; + +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; +import deprecated from '@wordpress/deprecated'; + +function stopPropagation( event: MouseEvent ) { + event.stopPropagation(); +} + +type DivProps = ComponentPropsWithoutRef< 'div' >; + +const IsolatedEventContainer = forwardRef< HTMLDivElement, DivProps >( + ( props, ref ) => { + deprecated( 'wp.components.IsolatedEventContainer', { + since: '5.7', + } ); + + // Disable reason: this stops certain events from propagating outside of the component. + // - onMouseDown is disabled as this can cause interactions with other DOM elements. + /* eslint-disable jsx-a11y/no-static-element-interactions */ + return
; + /* eslint-enable jsx-a11y/no-static-element-interactions */ + } +); + +export default IsolatedEventContainer; From 04407b948f8b1241830eb4b3f90b69cd58e5fd36 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 25 Oct 2022 10:50:40 +0200 Subject: [PATCH 2/9] FormFileUpload: fix render() call because void is not assignable to undefined --- .../components/src/form-file-upload/index.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/components/src/form-file-upload/index.tsx b/packages/components/src/form-file-upload/index.tsx index ce2392365fe1a..c896d6c06c7aa 100644 --- a/packages/components/src/form-file-upload/index.tsx +++ b/packages/components/src/form-file-upload/index.tsx @@ -40,13 +40,17 @@ export function FormFileUpload( { ref.current?.click(); }; - const ui = render ? ( - render( { openFileDialog } ) - ) : ( - - ); + let ui = null; + if ( render ) { + render( { openFileDialog } ); + } else { + ui = ( + + ); + } + return (
{ ui } From a18b467ca1c3c2c82ca6c324906be5823fd0b114 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 25 Oct 2022 10:51:40 +0200 Subject: [PATCH 3/9] ToggleControl: enhance help prop type to allow functions --- packages/components/src/toggle-control/index.tsx | 2 +- packages/components/src/toggle-control/types.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/components/src/toggle-control/index.tsx b/packages/components/src/toggle-control/index.tsx index cb31677a356ec..cf0aca2fd1827 100644 --- a/packages/components/src/toggle-control/index.tsx +++ b/packages/components/src/toggle-control/index.tsx @@ -43,7 +43,7 @@ import { space } from '../ui/utils/space'; export function ToggleControl( { __nextHasNoMarginBottom, label, - checked, + checked = false, help, className, onChange, diff --git a/packages/components/src/toggle-control/types.ts b/packages/components/src/toggle-control/types.ts index 3ad8d815f39d9..568c44fb64af0 100644 --- a/packages/components/src/toggle-control/types.ts +++ b/packages/components/src/toggle-control/types.ts @@ -13,10 +13,8 @@ export type ToggleControlProps = Pick< FormToggleProps, 'checked' | 'disabled' > & - Pick< - BaseControlProps, - '__nextHasNoMarginBottom' | 'help' | 'className' - > & { + Pick< BaseControlProps, '__nextHasNoMarginBottom' | 'className' > & { + help?: ReactNode | ( ( checked: boolean ) => ReactNode ); /** * The label for the toggle. */ From 2c8b79f57e6adaeeb7d52e72599126b9a7c91235 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 25 Oct 2022 10:53:21 +0200 Subject: [PATCH 4/9] Popover: add types to arrowRef --- packages/components/src/popover/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 25bb157247a36..fba4dedb7a004 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -252,7 +252,7 @@ const UnforwardedPopover = ( } ); } - const arrowRef = useRef( null ); + const arrowRef = useRef< HTMLElement | null >( null ); const [ fallbackReferenceElement, setFallbackReferenceElement ] = useState< HTMLSpanElement | null >( null ); @@ -385,7 +385,7 @@ const UnforwardedPopover = ( } ); const arrowCallbackRef = useCallback( - ( node ) => { + ( node: HTMLElement | null ) => { arrowRef.current = node; update(); }, From 6a0f4a8ac3b4fc715db65002d62de4c633469fbc Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 25 Oct 2022 14:05:47 +0200 Subject: [PATCH 5/9] useFocusOutside: use param type annotation closer to the actual function --- .../src/hooks/use-focus-outside/index.js | 80 ++++++++++--------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/packages/compose/src/hooks/use-focus-outside/index.js b/packages/compose/src/hooks/use-focus-outside/index.js index fdccb6ea01330..045c946bbcfec 100644 --- a/packages/compose/src/hooks/use-focus-outside/index.js +++ b/packages/compose/src/hooks/use-focus-outside/index.js @@ -125,19 +125,23 @@ export default function useFocusOutside( onFocusOutside ) { * intends to normalize this as treating click on buttons as focus. * * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus - * - * @param {SyntheticEvent} event Event for mousedown or mouseup. */ - const normalizeButtonFocus = useCallback( ( event ) => { - const { type, target } = event; - const isInteractionEnd = [ 'mouseup', 'touchend' ].includes( type ); - - if ( isInteractionEnd ) { - preventBlurCheck.current = false; - } else if ( isFocusNormalizedButton( target ) ) { - preventBlurCheck.current = true; - } - }, [] ); + const normalizeButtonFocus = useCallback( + /** + * @param {SyntheticEvent} event Event for mousedown or mouseup. + */ + ( event ) => { + const { type, target } = event; + const isInteractionEnd = [ 'mouseup', 'touchend' ].includes( type ); + + if ( isInteractionEnd ) { + preventBlurCheck.current = false; + } else if ( isFocusNormalizedButton( target ) ) { + preventBlurCheck.current = true; + } + }, + [] + ); /** * A callback triggered when a blur event occurs on the element the handler @@ -145,34 +149,38 @@ export default function useFocusOutside( onFocusOutside ) { * * Calls the `onFocusOutside` callback in an immediate timeout if focus has * move outside the bound element and is still within the document. - * - * @param {SyntheticEvent} event Blur event. */ - const queueBlurCheck = useCallback( ( event ) => { - // React does not allow using an event reference asynchronously - // due to recycling behavior, except when explicitly persisted. - event.persist(); - - // Skip blur check if clicking button. See `normalizeButtonFocus`. - if ( preventBlurCheck.current ) { - return; - } - - blurCheckTimeoutId.current = setTimeout( () => { - // If document is not focused then focus should remain - // inside the wrapped component and therefore we cancel - // this blur event thereby leaving focus in place. - // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus. - if ( ! document.hasFocus() ) { - event.preventDefault(); + const queueBlurCheck = useCallback( + /** + * @param {SyntheticEvent} event Blur event. + */ + ( event ) => { + // React does not allow using an event reference asynchronously + // due to recycling behavior, except when explicitly persisted. + event.persist(); + + // Skip blur check if clicking button. See `normalizeButtonFocus`. + if ( preventBlurCheck.current ) { return; } - if ( 'function' === typeof currentOnFocusOutside.current ) { - currentOnFocusOutside.current( event ); - } - }, 0 ); - }, [] ); + blurCheckTimeoutId.current = setTimeout( () => { + // If document is not focused then focus should remain + // inside the wrapped component and therefore we cancel + // this blur event thereby leaving focus in place. + // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus. + if ( ! document.hasFocus() ) { + event.preventDefault(); + return; + } + + if ( 'function' === typeof currentOnFocusOutside.current ) { + currentOnFocusOutside.current( event ); + } + }, 0 ); + }, + [] + ); return { onFocus: cancelBlurCheck, From b7b5064d3ffda1a121f1a2fc43c712f10f63759b Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 25 Oct 2022 14:06:37 +0200 Subject: [PATCH 6/9] useDialog: specify type for event listener, use DOM KeyboardEvent type --- packages/compose/src/hooks/use-dialog/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compose/src/hooks/use-dialog/index.ts b/packages/compose/src/hooks/use-dialog/index.ts index 28086799c0ded..b8ec91a5f206a 100644 --- a/packages/compose/src/hooks/use-dialog/index.ts +++ b/packages/compose/src/hooks/use-dialog/index.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { KeyboardEvent, RefCallback, SyntheticEvent } from 'react'; +import type { RefCallback, SyntheticEvent } from 'react'; /** * WordPress dependencies @@ -64,7 +64,7 @@ function useDialog( options: DialogOptions ): useDialogReturn { currentOptions.current.onClose(); } } ); - const closeOnEscapeRef = useCallback( ( node ) => { + const closeOnEscapeRef = useCallback( ( node: HTMLElement ) => { if ( ! node ) { return; } From 4a0e4f6c358292dbd06d7da30279e649da52a72f Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 25 Oct 2022 15:22:39 +0200 Subject: [PATCH 7/9] ToggleControl fix: show dynamic help label only for controlled components --- .../components/src/toggle-control/index.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/components/src/toggle-control/index.tsx b/packages/components/src/toggle-control/index.tsx index cf0aca2fd1827..4dfcda30e7cab 100644 --- a/packages/components/src/toggle-control/index.tsx +++ b/packages/components/src/toggle-control/index.tsx @@ -43,7 +43,7 @@ import { space } from '../ui/utils/space'; export function ToggleControl( { __nextHasNoMarginBottom, label, - checked = false, + checked, help, className, onChange, @@ -64,8 +64,19 @@ export function ToggleControl( { let describedBy, helpLabel; if ( help ) { - describedBy = id + '__help'; - helpLabel = typeof help === 'function' ? help( checked ) : help; + if ( typeof help === 'function' ) { + // `help` as a function works only for controlled components where + // `checked` is passed down from parent component. Uncontrolled + // component can show only a static help label. + if ( checked !== undefined ) { + helpLabel = help( checked ); + } + } else { + helpLabel = help; + } + if ( helpLabel ) { + describedBy = id + '__help'; + } } return ( From 233bd8e09d7e7dcbae405daa87069966555f90b9 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 25 Oct 2022 19:02:20 +0200 Subject: [PATCH 8/9] FormFileUpload: fix the render prop type and behavior --- .../components/src/form-file-upload/README.md | 4 +--- .../components/src/form-file-upload/index.tsx | 17 +++++++---------- .../components/src/form-file-upload/types.ts | 9 +++++---- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/components/src/form-file-upload/README.md b/packages/components/src/form-file-upload/README.md index ca3f356fade0f..38dd50dd03915 100644 --- a/packages/components/src/form-file-upload/README.md +++ b/packages/components/src/form-file-upload/README.md @@ -81,9 +81,7 @@ This can be useful when you want to force a `change` event to fire when the user ### render -Optional callback function used to render the UI. If passed the component does not render any UI and calls this function to render it. - -This function receives an object with the property `openFileDialog`. The property is a function that when called opens the browser window to upload files. +Optional callback function used to render the UI. If passed, the component does not render the default UI (a button) and calls this function to render it. The function receives an object with property `openFileDialog`, a function that, when called, opens the browser native file upload modal window. - Type: `Function` - Required: No diff --git a/packages/components/src/form-file-upload/index.tsx b/packages/components/src/form-file-upload/index.tsx index c896d6c06c7aa..608fdf837300f 100644 --- a/packages/components/src/form-file-upload/index.tsx +++ b/packages/components/src/form-file-upload/index.tsx @@ -40,16 +40,13 @@ export function FormFileUpload( { ref.current?.click(); }; - let ui = null; - if ( render ) { - render( { openFileDialog } ); - } else { - ui = ( - - ); - } + const ui = render ? ( + render( { openFileDialog } ) + ) : ( + + ); return (
diff --git a/packages/components/src/form-file-upload/types.ts b/packages/components/src/form-file-upload/types.ts index 4e4281383d087..5b9054fa869ee 100644 --- a/packages/components/src/form-file-upload/types.ts +++ b/packages/components/src/form-file-upload/types.ts @@ -55,9 +55,10 @@ export type FormFileUploadProps = { /** * Optional callback function used to render the UI. * - * If passed, the component does not render any UI and calls this function to render it. - * This function receives an object with the property `openFileDialog`. - * The property is a function that when called opens the browser window to upload files. + * If passed, the component does not render the default UI (a button) and + * calls this function to render it. The function receives an object with + * property `openFileDialog`, a function that, when called, opens the browser + * native file upload modal window. */ - render?: ( arg: { openFileDialog: () => void } ) => void; + render?: ( arg: { openFileDialog: () => void } ) => ReactNode; }; From fb0086d2d35e9def002e55d49a6ffc1179dfe253 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Wed, 26 Oct 2022 07:54:40 +0200 Subject: [PATCH 9/9] ToggleControl: add changelog entry and update docs --- packages/components/CHANGELOG.md | 1 + packages/components/src/toggle-control/README.md | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 3881fea0860ff..6ac59eb1ef0e1 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,6 +9,7 @@ ## Enhancements - `FontSizePicker`: Improved slider design when `withSlider` is set ([#44598](https://github.com/WordPress/gutenberg/pull/44598)). +- `ToggleControl`: Improved types for the `help` prop, covering the dynamic render function option, and enabled the dynamic `help` behavior only for a controlled component ([#45279](https://github.com/WordPress/gutenberg/pull/45279)). ### Bug Fix diff --git a/packages/components/src/toggle-control/README.md b/packages/components/src/toggle-control/README.md index e149dcd5f4d2c..d22c99a414d16 100644 --- a/packages/components/src/toggle-control/README.md +++ b/packages/components/src/toggle-control/README.md @@ -44,14 +44,16 @@ If this property is added, a label will be generated using label property as the ### help If this property is added, a help text will be generated using help property as the content. +For controlled components the `help` prop can also be a function which will return a help text +dynamically depending on the boolean `checked` parameter. -- Type: `String|WPElement` +- Type: `String|WPElement|Function` - Required: No ### checked 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. +If no value is passed the toggle will be an uncontrolled component with unchecked initial value. - Type: `Boolean` - Required: No @@ -74,5 +76,5 @@ A function that receives the checked state (boolean) as input. The class that will be added with `components-base-control` and `components-toggle-control` to the classes of the wrapper div. If no className is passed only `components-base-control` and `components-toggle-control` are used. -Type: String -Required: No +- Type: `String` +- Required: No