Skip to content

Commit

Permalink
Try a controlled component
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewserong committed Sep 7, 2023
1 parent 19f88d3 commit 67801dd
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,26 +191,25 @@ export function BlockSettingsDropdown( {
const parentBlockIsSelected =
selectedBlockClientIds?.includes( firstParentClientId );

// Only override the `open` prop if the current block is not the one that
// opened the menu. The logic here is to ensure that non-current
// block menus are automatically closed when a new block menu is opened.
// This is useful for cases where focus is not present in the current window.
// All other behavior of the drop down menu should be otherwise unaffected.
const open =
! currentClientId || openedBlockSettingsMenu === currentClientId
? undefined
: false;
// When a currentClientId is in use, treat the menu as a controlled component.
// This ensures that only one block settings menu is open at a time.
const open = ! currentClientId
? undefined
: openedBlockSettingsMenu === currentClientId || false;

const onToggle = useCallback(
( localOpen ) => {
// When the current menu is opened, update the state to reflect
// the new current menu. This allows all other instances of the
// menu to close if they already open.
if ( localOpen ) {
if ( localOpen && openedBlockSettingsMenu !== currentClientId ) {
setOpenedBlockSettingsMenu( currentClientId );
} else if (
! localOpen &&
openedBlockSettingsMenu &&
openedBlockSettingsMenu === currentClientId
) {
setOpenedBlockSettingsMenu( undefined );
}
},
[ currentClientId, setOpenedBlockSettingsMenu ]
[ currentClientId, openedBlockSettingsMenu, setOpenedBlockSettingsMenu ]
);

return (
Expand Down
11 changes: 9 additions & 2 deletions packages/components/src/dropdown-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,20 @@ A class name to apply to the dropdown menu's toggle element wrapper.

- Required: No

### `open`: `boolean`
#### `defaultOpen`: `boolean`

The initial open state of the dropdown.

- Required: No
- Default: `false`

#### `open`: `boolean`

Control whether the dropdown is open or not.

- Required: No

### `onToggle`: `( willOpen: boolean ) => void`
#### `onToggle`: `( willOpen: boolean ) => void`

A callback invoked when the state of the popover changes from open to closed and vice versa.

Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/dropdown-menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function UnconnectedDropdownMenu( dropdownMenuProps: DropdownMenuProps ) {
className,
controls,
icon = menu,
defaultOpen,
open: openProp,
label,
popoverProps,
Expand Down Expand Up @@ -94,6 +95,7 @@ function UnconnectedDropdownMenu( dropdownMenuProps: DropdownMenuProps ) {
return (
<Dropdown
className={ className }
defaultOpen={ defaultOpen }
open={ openProp }
onToggle={ onToggleProp }
popoverProps={ mergedPopoverProps }
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/dropdown-menu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export type DropdownMenuProps = {
* @default "menu"
*/
icon?: IconProps[ 'icon' ] | null;
/**
* The initial open state of the dropdown.
*
* @default false
*/
defaultOpen?: boolean;
/**
* Whether the dropdown is opened or not.
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/components/src/dropdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ Set this to customize the text that is shown in the dropdown's header when it is

- Required: No

### `defaultOpen`: `boolean`

The initial open state of the dropdown.

- Required: No
- Default: `false`

### `open`: `boolean`

Control whether the dropdown is open or not.
Expand Down
33 changes: 13 additions & 20 deletions packages/components/src/dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,7 @@ import deprecated from '@wordpress/deprecated';
import { contextConnect, useContextSystem } from '../ui/context';
import Popover from '../popover';
import type { DropdownProps, DropdownInternalContext } from './types';

function useObservableState(
initialState: boolean,
onStateChange?: ( newState: boolean ) => void
) {
const [ state, setState ] = useState( initialState );
return [
state,
( value: boolean ) => {
setState( value );
if ( onStateChange ) {
onStateChange( value );
}
},
] as const;
}
import { useControlledValue } from '../utils/hooks';

const UnconnectedDropdown = (
props: DropdownProps,
Expand All @@ -50,6 +35,7 @@ const UnconnectedDropdown = (
onClose,
onToggle,
style,
defaultOpen = false,
open: openProp,

// Deprecated props
Expand All @@ -75,10 +61,17 @@ const UnconnectedDropdown = (
const [ fallbackPopoverAnchor, setFallbackPopoverAnchor ] =
useState< HTMLDivElement | null >( null );
const containerRef = useRef< HTMLDivElement >();
const [ isOpenState, setIsOpen ] = useObservableState( false, onToggle );

// Allow provided `isOpen` prop to override internal state.
const isOpen = openProp ?? isOpenState;
const [ isOpenValue, setIsOpen ] = useControlledValue( {
defaultValue: defaultOpen,
onChange: onToggle,
value: openProp,
} );

// `renderToggle` and `renderContent` expect `isOpen` to be a boolean, but
// TypeScript believes it can be `undefined` because of how `useControlledValue`
// is typed. This assertion is safe because `isOpen` is initialized to `false`
// and `setIsOpen` is only called with `false` or `true`.
const isOpen = isOpenValue || false;

useEffect(
() => () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/dropdown/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export type DropdownProps = {
* as a child of the container node.
*/
contentClassName?: string;
/**
* The initial open state of the dropdown.
*
* @default false
*/
defaultOpen?: boolean;
/**
* Opt-in prop to show popovers fullscreen on mobile.
*
Expand Down

0 comments on commit 67801dd

Please sign in to comment.