Skip to content

Commit

Permalink
Block Editor: Introduce block locking UI (#39183)
Browse files Browse the repository at this point in the history
* Block Locking: Introduce UI

* Add lock button to the block toolbar

* Specify rootClientId in modal

* Fix typo

* Update text and fix typos

* Remove top separator and update icon position

* Display toolbar icon if any of the restrictions is active

* Add icons to menu item

* Update labels

* Move block settings menu item

* Fix menu item label condition

* Add basic e2e tests

* Update text

* Update toolbar button spacing

* Remove block title suffix

* Use rootClientId in menu item

* Don't export new components

* Remove extra div
  • Loading branch information
Mamaduka authored Mar 15, 2022
1 parent 45c37fb commit dfd422a
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 7 deletions.
1 change: 1 addition & 0 deletions packages/base-styles/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ $z-layers: (
// Should be above the popover (dropdown)
".reusable-blocks-menu-items__convert-modal": 1000001,
".edit-site-create-template-part-modal": 1000001,
".block-editor-block-lock-modal": 1000001,

// Note: The ConfirmDialog component's z-index is being set to 1000001 in packages/components/src/confirm-dialog/styles.ts
// because it uses emotion and not sass. We need it to render on top its parent popover.
Expand Down
3 changes: 3 additions & 0 deletions packages/block-editor/src/components/block-lock/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as BlockLockMenuItem } from './menu-item';
export { default as BlockLockModal } from './modal';
export { default as BlockLockToolbar } from './toolbar';
52 changes: 52 additions & 0 deletions packages/block-editor/src/components/block-lock/menu-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useReducer } from '@wordpress/element';
import { MenuItem } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { lock, unlock } from '@wordpress/icons';

/**
* Internal dependencies
*/
import BlockLockModal from './modal';
import { store as blockEditorStore } from '../../store';

export default function BlockLockMenuItem( { clientId } ) {
const { isLocked } = useSelect(
( select ) => {
const {
canMoveBlock,
canRemoveBlock,
getBlockRootClientId,
} = select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );

return {
isLocked:
! canMoveBlock( clientId, rootClientId ) ||
! canRemoveBlock( clientId, rootClientId ),
};
},
[ clientId ]
);

const [ isModalOpen, toggleModal ] = useReducer(
( isActive ) => ! isActive,
false
);

const label = isLocked ? __( 'Unlock' ) : __( 'Lock' );

return (
<>
<MenuItem icon={ isLocked ? unlock : lock } onClick={ toggleModal }>
{ label }
</MenuItem>
{ isModalOpen && (
<BlockLockModal clientId={ clientId } onClose={ toggleModal } />
) }
</>
);
}
165 changes: 165 additions & 0 deletions packages/block-editor/src/components/block-lock/modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
import {
Button,
CheckboxControl,
Flex,
FlexItem,
Icon,
Modal,
} from '@wordpress/components';
import { dragHandle, trash } from '@wordpress/icons';
import { useInstanceId } from '@wordpress/compose';
import { useDispatch, useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import useBlockDisplayInformation from '../use-block-display-information';
import { store as blockEditorStore } from '../../store';

export default function BlockLockModal( { clientId, onClose } ) {
const [ lock, setLock ] = useState( { move: false, remove: false } );
const { canMove, canRemove } = useSelect(
( select ) => {
const {
canMoveBlock,
canRemoveBlock,
getBlockRootClientId,
} = select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );

return {
canMove: canMoveBlock( clientId, rootClientId ),
canRemove: canRemoveBlock( clientId, rootClientId ),
};
},
[ clientId ]
);
const { updateBlockAttributes } = useDispatch( blockEditorStore );
const blockInformation = useBlockDisplayInformation( clientId );
const instanceId = useInstanceId(
BlockLockModal,
'block-editor-block-lock-modal__options-title'
);

useEffect( () => {
setLock( {
move: ! canMove,
remove: ! canRemove,
} );
}, [ canMove, canRemove ] );

const isAllChecked = Object.values( lock ).every( Boolean );

let ariaChecked;
if ( isAllChecked ) {
ariaChecked = 'true';
} else if ( Object.values( lock ).some( Boolean ) ) {
ariaChecked = 'mixed';
} else {
ariaChecked = 'false';
}

return (
<Modal
title={ sprintf(
/* translators: %s: Name of the block. */
__( 'Lock %s' ),
blockInformation.title
) }
overlayClassName="block-editor-block-lock-modal"
closeLabel={ __( 'Close' ) }
onRequestClose={ onClose }
>
<form
onSubmit={ ( event ) => {
event.preventDefault();
updateBlockAttributes( [ clientId ], { lock } );
onClose();
} }
>
<p>
{ __(
'Choose specific attributes to restrict or lock all available options.'
) }
</p>
<div
role="group"
aria-labelledby={ instanceId }
className="block-editor-block-lock-modal__options"
>
<CheckboxControl
className="block-editor-block-lock-modal__options-title"
label={
<span id={ instanceId }>{ __( 'Lock all' ) }</span>
}
checked={ isAllChecked }
aria-checked={ ariaChecked }
onChange={ ( newValue ) =>
setLock( {
move: newValue,
remove: newValue,
} )
}
/>
<ul className="block-editor-block-lock-modal__checklist">
<li className="block-editor-block-lock-modal__checklist-item">
<CheckboxControl
label={
<>
{ __( 'Disable movement' ) }
<Icon icon={ dragHandle } />
</>
}
checked={ lock.move }
onChange={ ( move ) =>
setLock( ( prevLock ) => ( {
...prevLock,
move,
} ) )
}
/>
</li>
<li className="block-editor-block-lock-modal__checklist-item">
<CheckboxControl
label={
<>
{ __( 'Prevent removal' ) }
<Icon icon={ trash } />
</>
}
checked={ lock.remove }
onChange={ ( remove ) =>
setLock( ( prevLock ) => ( {
...prevLock,
remove,
} ) )
}
/>
</li>
</ul>
</div>
<Flex
className="block-editor-block-lock-modal__actions"
justify="flex-end"
expanded={ false }
>
<FlexItem>
<Button variant="tertiary" onClick={ onClose }>
{ __( 'Cancel' ) }
</Button>
</FlexItem>
<FlexItem>
<Button variant="primary" type="submit">
{ __( 'Apply' ) }
</Button>
</FlexItem>
</Flex>
</form>
</Modal>
);
}
67 changes: 67 additions & 0 deletions packages/block-editor/src/components/block-lock/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
.block-editor-block-lock-modal {
z-index: z-index(".block-editor-block-lock-modal");

.components-modal__frame {
@include break-small() {
max-width: $break-mobile;
}
}
}

.block-editor-block-lock-modal__checklist {
margin: 0;
}

.block-editor-block-lock-modal__options-title {
border-bottom: 1px solid $gray-300;
padding: $grid-unit-15 0;

.components-checkbox-control__label {
font-weight: 600;
}

.components-base-control__field {
align-items: center;
display: flex;
margin: 0;
}
}
.block-editor-block-lock-modal__checklist-item {
border-bottom: 1px solid $gray-300;
margin-bottom: 0;
padding: $grid-unit-15 0 $grid-unit-15 $grid-unit-15;

.components-base-control__field {
align-items: center;
display: flex;
margin: 0;
}

.components-checkbox-control__label {
display: flex;
align-items: center;
justify-content: space-between;
flex-grow: 1;

svg {
margin-right: $grid-unit-15;
fill: $gray-900;
}
}
}

.block-editor-block-lock-modal__actions {
margin-top: $grid-unit-30;
}

.block-editor-block-lock-toolbar {
.components-button.has-icon {
min-width: $button-size-small + $grid-unit-15 !important;
padding-left: 0 !important;

&:focus::before {
left: 0 !important;
right: $grid-unit-15 !important;
}
}
}
58 changes: 58 additions & 0 deletions packages/block-editor/src/components/block-lock/toolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { ToolbarButton, ToolbarGroup } from '@wordpress/components';
import { useReducer } from '@wordpress/element';
import { lock } from '@wordpress/icons';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import BlockLockModal from './modal';
import useBlockDisplayInformation from '../use-block-display-information';
import { store as blockEditorStore } from '../../store';

export default function BlockLockToolbar( { clientId } ) {
const blockInformation = useBlockDisplayInformation( clientId );
const { canMove, canRemove } = useSelect(
( select ) => {
const { canMoveBlock, canRemoveBlock } = select( blockEditorStore );

return {
canMove: canMoveBlock( clientId ),
canRemove: canRemoveBlock( clientId ),
};
},
[ clientId ]
);

const [ isModalOpen, toggleModal ] = useReducer(
( isActive ) => ! isActive,
false
);

if ( canMove && canRemove ) {
return null;
}

return (
<>
<ToolbarGroup className="block-editor-block-lock-toolbar">
<ToolbarButton
icon={ lock }
label={ sprintf(
/* translators: %s: block name */
__( 'Unlock %s' ),
blockInformation.title
) }
onClick={ toggleModal }
/>
</ToolbarGroup>
{ isModalOpen && (
<BlockLockModal clientId={ clientId } onClose={ toggleModal } />
) }
</>
);
}
Loading

0 comments on commit dfd422a

Please sign in to comment.