Skip to content

Commit

Permalink
Try: use state to determine inactive block dropdown menus to close
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewserong committed Aug 31, 2023
1 parent a04bf22 commit 130c0d7
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import BlockHTMLConvertButton from './block-html-convert-button';
import __unstableBlockSettingsMenuFirstItem from './block-settings-menu-first-item';
import BlockSettingsMenuControls from '../block-settings-menu-controls';
import { store as blockEditorStore } from '../../store';
import { unlock } from '../../lock-unlock';
import { useShowHoveredOrFocusedGestures } from '../block-toolbar/utils';

const POPOVER_PROPS = {
Expand All @@ -47,13 +48,17 @@ function CopyMenuItem( { blocks, onCopy, label } ) {
}

export function BlockSettingsDropdown( {
block,
clientIds,
__experimentalSelectBlock,
children,
popoverProps,
__unstableDisplayLocation,
...props
} ) {
// Get the client id of the current block for this menu, if one is set.
const currentClientId = block?.clientId;

const blockClientIds = Array.isArray( clientIds )
? clientIds
: [ clientIds ];
Expand Down Expand Up @@ -103,6 +108,15 @@ export function BlockSettingsDropdown( {
const { getBlockOrder, getSelectedBlockClientIds } =
useSelect( blockEditorStore );

const { openedBlockMenu } = useSelect( ( select ) => {
const { getOpenedBlockMenu } = unlock( select( blockEditorStore ) );
return {
openedBlockMenu: getOpenedBlockMenu(),
};
} );

const { setOpenedBlockMenu } = unlock( useDispatch( blockEditorStore ) );

const shortcuts = useSelect( ( select ) => {
const { getShortcutRepresentation } = select( keyboardShortcutsStore );
return {
Expand Down Expand Up @@ -175,6 +189,23 @@ export function BlockSettingsDropdown( {
const parentBlockIsSelected =
selectedBlockClientIds?.includes( firstParentClientId );

// Only override the isOpen prop if the current block is not the one that
// opened the menu. The logic here is only to ensure that non-current
// block menus are automatically closed when a new block menu is opened.
const isOpen =
! currentClientId || openedBlockMenu === currentClientId
? undefined
: false;

const onToggle = useCallback(
( localIsOpen ) => {
if ( localIsOpen ) {
setOpenedBlockMenu( currentClientId );
}
},
[ currentClientId, setOpenedBlockMenu ]
);

return (
<BlockActions
clientIds={ clientIds }
Expand All @@ -199,6 +230,8 @@ export function BlockSettingsDropdown( {
icon={ moreVertical }
label={ __( 'Options' ) }
className="block-editor-block-settings-menu"
isOpen={ isOpen }
onToggle={ onToggle }
popoverProps={ { ...POPOVER_PROPS, ...popoverProps } }
noIcons
menuProps={ {
Expand Down
13 changes: 13 additions & 0 deletions packages/block-editor/src/store/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,16 @@ export function setBlockRemovalRules( rules = false ) {
rules,
};
}

/**
* Sets the client ID of the block menu that is currently open.
*
* @param {string} clientId The block client ID.
* @return {Object} Action object.
*/
export function setOpenedBlockMenu( clientId = '' ) {
return {
type: 'SET_OPENED_BLOCK_MENU',
clientId,
};
}
10 changes: 10 additions & 0 deletions packages/block-editor/src/store/private-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,13 @@ export function getRemovalPromptData( state ) {
export function getBlockRemovalRules( state ) {
return state.blockRemovalRules;
}

/**
* Returns the client ID of the block menu that is currently open.
*
* @param {Object} state Global application state.
* @return {string|undefined} The client ID of the block menu that is currently open.
*/
export function getOpenedBlockMenu( state ) {
return state.openedBlockMenu;
}
9 changes: 9 additions & 0 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,14 @@ export function blockEditingModes( state = new Map(), action ) {
return state;
}

export function openedBlockMenu( state = null, action ) {
switch ( action.type ) {
case 'SET_OPENED_BLOCK_MENU':
return action.clientId || null;
}
return state;
}

const combinedReducers = combineReducers( {
blocks,
isTyping,
Expand All @@ -1938,6 +1946,7 @@ const combinedReducers = combineReducers( {
blockEditingModes,
removalPromptData,
blockRemovalRules,
openedBlockMenu,
} );

function withAutomaticChangeReset( reducer ) {
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/dropdown-menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ function UnconnectedDropdownMenu( dropdownMenuProps: DropdownMenuProps ) {
className,
controls,
icon = menu,
isOpen: isOpenProp,
label,
popoverProps,
toggleProps,
onToggle: onToggleProp,
menuProps,
disableOpenOnArrowDown = false,
text,
Expand Down Expand Up @@ -92,6 +94,8 @@ function UnconnectedDropdownMenu( dropdownMenuProps: DropdownMenuProps ) {
return (
<Dropdown
className={ className }
isOpen={ isOpenProp }
onToggle={ onToggleProp }
popoverProps={ mergedPopoverProps }
renderToggle={ ( { isOpen, onToggle } ) => {
const openOnArrowDown = ( event: React.KeyboardEvent ) => {
Expand Down
9 changes: 9 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,10 @@ export type DropdownMenuProps = {
* @default "menu"
*/
icon?: IconProps[ 'icon' ] | null;
/**
* Whether the dropdown is opened or not.
*/
isOpen?: boolean;
/**
* A human-readable label to present as accessibility text on the focused
* collapsed menu button.
Expand All @@ -86,6 +90,11 @@ export type DropdownMenuProps = {
* set with `position` prop.
*/
popoverProps?: DropdownProps[ 'popoverProps' ];
/**
* A function that is called any time the menu is toggled from its closed
* state to its opened state, or vice versa.
*/
onToggle?: ( next: boolean ) => void;
/**
* Properties of `toggleProps` object will be passed as props to the nested
* `Button` component in the `renderToggle` implementation of the `Dropdown`
Expand Down
6 changes: 5 additions & 1 deletion packages/components/src/dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const UnconnectedDropdown = (
onClose,
onToggle,
style,
isOpen: isOpenProp,

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

// Allow provided `isOpen` prop to override internal state.
const isOpen = isOpenProp ?? isOpenState;

useEffect(
() => () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/dropdown/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export type DropdownProps = {
* when it is fullscreen on mobile.
*/
headerTitle?: string;
/**
* Whether the dropdown is opened or not.
*/
isOpen?: boolean;
/**
* A callback invoked when the popover should be closed.
*/
Expand Down

0 comments on commit 130c0d7

Please sign in to comment.