Skip to content

Commit

Permalink
Update some React 18 related types (#45279)
Browse files Browse the repository at this point in the history
* IsolatedEventContainer: ensure there is type for children

* FormFileUpload: fix render() call because void is not assignable to undefined

* ToggleControl: enhance help prop type to allow functions

* Popover: add types to arrowRef

* useFocusOutside: use param type annotation closer to the actual function

* useDialog: specify type for event listener, use DOM KeyboardEvent type

* ToggleControl fix: show dynamic help label only for controlled components

* FormFileUpload: fix the render prop type and behavior

* ToggleControl: add changelog entry and update docs
  • Loading branch information
jsnajdr authored Oct 26, 2022
1 parent 75dfcf0 commit 362d820
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 85 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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

Expand Down
4 changes: 1 addition & 3 deletions packages/components/src/form-file-upload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions packages/components/src/form-file-upload/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function FormFileUpload( {
{ children }
</Button>
);

return (
<div className="components-form-file-upload">
{ ui }
Expand Down
9 changes: 5 additions & 4 deletions packages/components/src/form-file-upload/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
28 changes: 0 additions & 28 deletions packages/components/src/isolated-event-container/index.js

This file was deleted.

32 changes: 32 additions & 0 deletions packages/components/src/isolated-event-container/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <div { ...props } ref={ ref } onMouseDown={ stopPropagation } />;
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
);

export default IsolatedEventContainer;
4 changes: 2 additions & 2 deletions packages/components/src/popover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ const UnforwardedPopover = (
} );
}

const arrowRef = useRef( null );
const arrowRef = useRef< HTMLElement | null >( null );

const [ fallbackReferenceElement, setFallbackReferenceElement ] =
useState< HTMLSpanElement | null >( null );
Expand Down Expand Up @@ -361,7 +361,7 @@ const UnforwardedPopover = (
} );

const arrowCallbackRef = useCallback(
( node ) => {
( node: HTMLElement | null ) => {
arrowRef.current = node;
update();
},
Expand Down
10 changes: 6 additions & 4 deletions packages/components/src/toggle-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
15 changes: 13 additions & 2 deletions packages/components/src/toggle-control/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
6 changes: 2 additions & 4 deletions packages/components/src/toggle-control/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/compose/src/hooks/use-dialog/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { KeyboardEvent, RefCallback, SyntheticEvent } from 'react';
import type { RefCallback, SyntheticEvent } from 'react';

/**
* WordPress dependencies
Expand Down Expand Up @@ -64,7 +64,7 @@ function useDialog( options: DialogOptions ): useDialogReturn {
currentOptions.current.onClose();
}
} );
const closeOnEscapeRef = useCallback( ( node ) => {
const closeOnEscapeRef = useCallback( ( node: HTMLElement ) => {
if ( ! node ) {
return;
}
Expand Down
80 changes: 44 additions & 36 deletions packages/compose/src/hooks/use-focus-outside/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,54 +125,62 @@ 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
* is bound to.
*
* 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,
Expand Down

0 comments on commit 362d820

Please sign in to comment.