diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index d9d0ecbdd28b64..58646e592d38c1 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -809,7 +809,7 @@ export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) } case 'TOGGLE_SELECTION': return { - ...BLOCK_SELECTION_INITIAL_STATE, + ...state, isEnabled: action.isSelectionEnabled, }; case 'SELECTION_CHANGE': diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 7543fdad57dd8b..0cd6aef0481c61 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -1727,6 +1727,24 @@ describe( 'state', () => { expect( state1 ).toBe( original ); } ); + it( 'should maintain selection when toggling multi-selection', () => { + const original = deepFreeze( { + start: { clientId: 'ribs' }, + end: { clientId: 'ribs' }, + } ); + + const state = blockSelection( original, { + type: 'TOGGLE_SELECTION', + isSelectionEnabled: false, + } ); + + expect( state ).toEqual( { + start: { clientId: 'ribs' }, + end: { clientId: 'ribs' }, + isEnabled: false, + } ); + } ); + it( 'should unset multi selection', () => { const original = deepFreeze( { start: { clientId: 'ribs' }, end: { clientId: 'chicken' } } ); diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js index d573500cd551d9..e8dad030a8d5a6 100644 --- a/packages/block-library/src/columns/edit.js +++ b/packages/block-library/src/columns/edit.js @@ -34,6 +34,7 @@ import { getRedistributedColumnWidths, toWidthPrecision, } from './utils'; +import ColumnsResizer from './resizer'; /** * Allowed blocks constant is passed to InnerBlocks precisely as specified here. @@ -111,6 +112,8 @@ export function ColumnsEdit( { updateAlignment, updateColumns, clientId, + isSelected, + toggleSelection, } ) { const { verticalAlignment } = attributes; @@ -175,6 +178,12 @@ export function ColumnsEdit( { template={ count === 0 && ! forceUseTemplate ? null : template } templateLock="all" allowedBlocks={ ALLOWED_BLOCKS } /> + { isSelected && ( + + ) } ); diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index 6e9e2e60659114..e50b857107b56e 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -33,98 +33,138 @@ .wp-block-columns { display: block; +} - > .editor-inner-blocks > .editor-block-list__layout { - display: flex; +.wp-block-columns > .editor-inner-blocks > .editor-block-list__layout, +.wp-block-columns__resizer { + display: flex; - // Responsiveness: Allow wrapping on mobile. - flex-wrap: wrap; + // Responsiveness: Allow wrapping on mobile. + flex-wrap: wrap; - @include break-medium() { - flex-wrap: nowrap; + @include break-medium() { + flex-wrap: nowrap; + } + // Set full heights on Columns to enable vertical alignment preview + > [data-type="core/column"], + > [data-type="core/column"] > .editor-block-list__block-edit, + > [data-type="core/column"] > .editor-block-list__block-edit > div[data-block], + > [data-type="core/column"] > .editor-block-list__block-edit .block-core-columns, + > .wp-block-columns__resize-box { + @include flex-full-height(); + } + // Adjust the individual column block. + > [data-type="core/column"], + .wp-block-columns__resize-box { + + // On mobile, only a single column is shown, so match adjacent block paddings. + padding-left: 0; + padding-right: 0; + margin-left: -$block-padding; + margin-right: -$block-padding; + + // Prevent the columns from growing wider than their distributed sizes. + min-width: 0; + + // Prevent long unbroken words from overflowing. + word-break: break-word; // For back-compat. + overflow-wrap: break-word; // New standard. + + // Responsiveness: Show at most one columns on mobile. + flex-basis: 100%; + + // Beyond mobile, allow 2 columns. + @include break-small() { + flex-grow: 0; + margin-left: $block-padding; + margin-right: $block-padding; + + // TODO: This is not viable, since it will cause overflow of the + // columns content, but is necessary as demonstration of the impact + // on browser flex distribution on assigned width disparities. + flex-shrink: 0; + flex-basis: 50%; } - // Set full heights on Columns to enable vertical alignment preview - > [data-type="core/column"], - > [data-type="core/column"] > .editor-block-list__block-edit, - > [data-type="core/column"] > .editor-block-list__block-edit > div[data-block], - > [data-type="core/column"] > .editor-block-list__block-edit .block-core-columns { - @include flex-full-height(); + + // Add space between columns. Themes can customize this if they wish to work differently. + // This has to match the same padding applied in style.scss. + // Only apply this beyond the mobile breakpoint, as there's only a single column on mobile. + @include break-small() { + &:nth-child(even) { + margin-left: calc(#{$grid-size-large * 2} + #{$block-padding}); + } } - // Adjust the individual column block. - > [data-type="core/column"] { - - // On mobile, only a single column is shown, so match adjacent block paddings. - padding-left: 0; - padding-right: 0; - margin-left: -$block-padding; - margin-right: -$block-padding; - - // Prevent the columns from growing wider than their distributed sizes. - min-width: 0; - - // Prevent long unbroken words from overflowing. - word-break: break-word; // For back-compat. - overflow-wrap: break-word; // New standard. - - // Responsiveness: Show at most one columns on mobile. - flex-basis: 100%; - - // Beyond mobile, allow 2 columns. - @include break-small() { - flex-basis: calc(50% - (#{$grid-size-large} + #{$block-padding * 2})); - flex-grow: 0; - margin-left: $block-padding; - margin-right: $block-padding; + + // When columns are in a single row, add space before all except the first. + @include break-medium() { + &:not(:first-child) { + margin-left: calc(#{$grid-size-large * 2} + #{$block-padding}); } + } - // Add space between columns. Themes can customize this if they wish to work differently. - // This has to match the same padding applied in style.scss. - // Only apply this beyond the mobile breakpoint, as there's only a single column on mobile. - @include break-small() { - &:nth-child(even) { - margin-left: calc(#{$grid-size-large * 2} + #{$block-padding}); - } + > .editor-block-list__block-edit { + margin-top: 0; + margin-bottom: 0; + + // Remove Block "padding" so individual Column is flush with parent Columns + &::before { + left: 0; + right: 0; } - // When columns are in a single row, add space before all except the first. - @include break-medium() { - &:not(:first-child) { - margin-left: calc(#{$grid-size-large * 2} + #{$block-padding}); - } + > .editor-block-contextual-toolbar { + margin-left: -$border-width; } - > .editor-block-list__block-edit { + // Zero out margins. + > [data-block] { margin-top: 0; margin-bottom: 0; + } - // Remove Block "padding" so individual Column is flush with parent Columns - &::before { - left: 0; - right: 0; - } - - > .editor-block-contextual-toolbar { - margin-left: -$border-width; - } - - // Zero out margins. - > [data-block] { - margin-top: 0; - margin-bottom: 0; - } - - // The Columns block is a flex-container, therefore it nullifies margin collapsing. - // Therefore, blocks inside this will appear to create a double margin. - // We compensate for this using negative margins. - > div > .block-core-columns > .editor-inner-blocks { - margin-top: -$default-block-margin; - margin-bottom: -$default-block-margin; - } + // The Columns block is a flex-container, therefore it nullifies margin collapsing. + // Therefore, blocks inside this will appear to create a double margin. + // We compensate for this using negative margins. + > div > .block-core-columns > .editor-inner-blocks { + margin-top: -$default-block-margin; + margin-bottom: -$default-block-margin; } } } } +.wp-block-columns__resizer { + display: none; + + @include break-medium() { + display: flex; + } + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; +} + +.wp-block-columns__resize-box { + .wp-block-columns__resizer.is-resizing &:not(.is-active) { + opacity: 0; + } + + .wp-block-columns__resizer & { + // Reset natural flex-basis assigned above, deferring instead to the + // width assigned by ResizableBox. + flex-basis: auto; + } + + .components-resizable-box__handle { + pointer-events: auto; + margin-right: #{-1 * ($grid-size-large + $block-padding)}; + } +} + /** * Columns act as as a "passthrough container" * and therefore has its vertical margins/padding removed via negative margins diff --git a/packages/block-library/src/columns/resizer.js b/packages/block-library/src/columns/resizer.js new file mode 100644 index 00000000000000..9da222e9c48419 --- /dev/null +++ b/packages/block-library/src/columns/resizer.js @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +import { forEach, find, difference, over } from 'lodash'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useSelect, withDispatch } from '@wordpress/data'; +import { ResizableBox } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { + getEffectiveColumnWidth, + toWidthPrecision, + getTotalColumnsWidth, + getColumnWidths, + getAdjacentBlocks, + getRedistributedColumnWidths, +} from './utils'; + +function ColumnsResizer( { + clientId, + toggleSelection, + updateBlockAttributes, +} ) { + const [ activeResizeClientId, setActiveResizeClientId ] = useState( null ); + + const { columns } = useSelect( ( select ) => ( { + columns: select( 'core/block-editor' ).getBlocks( clientId ), + } ), [ clientId ] ); + + const classes = classnames( 'wp-block-columns__resizer', { + 'is-resizing': activeResizeClientId !== null, + } ); + + return ( +
+ { columns.map( ( column, index ) => { + const width = getEffectiveColumnWidth( column, columns.length ); + + function onResizeStop( event, direction, element ) { + const nextWidth = toWidthPrecision( parseFloat( element.style.width ) ); + const adjacentColumns = getAdjacentBlocks( columns, column.clientId ); + + // The occupied width is calculated as the sum of the new width + // and the total width of blocks _not_ in the adjacent set. + const occupiedWidth = nextWidth + getTotalColumnsWidth( + difference( columns, [ + find( columns, { clientId: column.clientId } ), + ...adjacentColumns, + ] ) + ); + + // Compute _all_ next column widths, in case the updated column + // is in the middle of a set of columns which don't yet have + // any explicit widths assigned (include updates to those not + // part of the adjacent blocks). + const nextColumnWidths = { + ...getColumnWidths( columns, columns.length ), + [ column.clientId ]: nextWidth, + ...getRedistributedColumnWidths( adjacentColumns, 100 - occupiedWidth, columns.length ), + }; + + forEach( nextColumnWidths, ( nextColumnWidth, columnClientId ) => { + updateBlockAttributes( columnClientId, { width: nextColumnWidth } ); + } ); + } + + return ( + setActiveResizeClientId( column.clientId ), + () => toggleSelection( false ), + ] ) } + onResizeStop={ over( [ + () => setActiveResizeClientId( null ), + () => toggleSelection( true ), + onResizeStop, + ] ) } + className={ classnames( + 'wp-block-columns__resize-box', + 'is-selected', + { 'is-active': activeResizeClientId === column.clientId } + ) } + /> + ); + } ) } +
+ ); +} + +export default withDispatch( ( dispatch ) => { + const { updateBlockAttributes } = dispatch( 'core/block-editor' ); + return { updateBlockAttributes }; +} )( ColumnsResizer );