) }
- { ( isEditingLink || ! value ) && ! isCreatingPage && (
+ { isEditing && (
<>
) }
-
-
-
-
>
) }
@@ -368,15 +350,34 @@ function LinkControl( {
/>
) }
- { showSettingsDrawer && (
-
-
-
- ) }
+
+ { showSettingsDrawer && (
+
+
+
+ ) }
+
+ { isEditing && (
+
+
+
+
+ ) }
+
+
{ renderControlBottom && renderControlBottom() }
);
diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss
index 51682a45c7c20b..cadb0ce6340e28 100644
--- a/packages/block-editor/src/components/link-control/style.scss
+++ b/packages/block-editor/src/components/link-control/style.scss
@@ -79,7 +79,6 @@ $preview-image-height: 140px;
display: flex;
flex-direction: row-reverse; // put "Cancel" on the left but retain DOM order.
justify-content: flex-start;
- margin: $grid-unit-20; // allow margin collapse for vertical spacing.
gap: $grid-unit-10;
}
@@ -427,9 +426,10 @@ $preview-image-height: 140px;
padding: 10px;
}
-.block-editor-link-control__tools {
+.block-editor-link-control__drawer {
display: flex;
align-items: center;
+ justify-content: space-between;
border-top: $border-width solid $gray-300;
margin: 0;
padding: $grid-unit-20;
diff --git a/packages/block-editor/src/experiments.js b/packages/block-editor/src/experiments.js
new file mode 100644
index 00000000000000..b217e14ec273be
--- /dev/null
+++ b/packages/block-editor/src/experiments.js
@@ -0,0 +1,23 @@
+/**
+ * WordPress dependencies
+ */
+import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/experiments';
+
+/**
+ * Internal dependencies
+ */
+import * as globalStyles from './components/global-styles';
+
+export const { lock, unlock } =
+ __dangerousOptInToUnstableAPIsOnlyForCoreModules(
+ 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
+ '@wordpress/block-editor'
+ );
+
+/**
+ * Experimental @wordpress/block-editor APIs.
+ */
+export const experiments = {};
+lock( experiments, {
+ ...globalStyles,
+} );
diff --git a/packages/block-editor/src/hooks/test/utils.js b/packages/block-editor/src/hooks/test/utils.js
index a919fad575312e..bfbced3195b7fb 100644
--- a/packages/block-editor/src/hooks/test/utils.js
+++ b/packages/block-editor/src/hooks/test/utils.js
@@ -7,9 +7,113 @@ import { applyFilters } from '@wordpress/hooks';
* Internal dependencies
*/
import '../anchor';
+import { immutableSet } from '../utils';
const noop = () => {};
+describe( 'immutableSet', () => {
+ describe( 'handling falsy values properly', () => {
+ it( 'should create a new object if `undefined` is passed', () => {
+ const result = immutableSet( undefined, 'test', 1 );
+
+ expect( result ).toEqual( { test: 1 } );
+ } );
+
+ it( 'should create a new object if `null` is passed', () => {
+ const result = immutableSet( null, 'test', 1 );
+
+ expect( result ).toEqual( { test: 1 } );
+ } );
+
+ it( 'should create a new object if `false` is passed', () => {
+ const result = immutableSet( false, 'test', 1 );
+
+ expect( result ).toEqual( { test: 1 } );
+ } );
+
+ it( 'should create a new object if `0` is passed', () => {
+ const result = immutableSet( 0, 'test', 1 );
+
+ expect( result ).toEqual( { test: 1 } );
+ } );
+
+ it( 'should create a new object if an empty string is passed', () => {
+ const result = immutableSet( '', 'test', 1 );
+
+ expect( result ).toEqual( { test: 1 } );
+ } );
+
+ it( 'should create a new object if a NaN is passed', () => {
+ const result = immutableSet( NaN, 'test', 1 );
+
+ expect( result ).toEqual( { test: 1 } );
+ } );
+ } );
+
+ describe( 'manages data assignment properly', () => {
+ it( 'assigns value properly when it does not exist', () => {
+ const result = immutableSet( {}, 'test', 1 );
+
+ expect( result ).toEqual( { test: 1 } );
+ } );
+
+ it( 'overrides existing values', () => {
+ const result = immutableSet( { test: 1 }, 'test', 2 );
+
+ expect( result ).toEqual( { test: 2 } );
+ } );
+
+ describe( 'with array notation access', () => {
+ it( 'assigns values at deeper levels', () => {
+ const result = immutableSet( {}, [ 'foo', 'bar', 'baz' ], 5 );
+
+ expect( result ).toEqual( { foo: { bar: { baz: 5 } } } );
+ } );
+
+ it( 'overrides existing values at deeper levels', () => {
+ const result = immutableSet(
+ { foo: { bar: { baz: 1 } } },
+ [ 'foo', 'bar', 'baz' ],
+ 5
+ );
+
+ expect( result ).toEqual( { foo: { bar: { baz: 5 } } } );
+ } );
+
+ it( 'keeps other properties intact', () => {
+ const result = immutableSet(
+ { foo: { bar: { baz: 1 } } },
+ [ 'foo', 'bar', 'test' ],
+ 5
+ );
+
+ expect( result ).toEqual( {
+ foo: { bar: { baz: 1, test: 5 } },
+ } );
+ } );
+ } );
+ } );
+
+ describe( 'does not mutate the original object', () => {
+ it( 'clones the object at the first level', () => {
+ const input = {};
+ const result = immutableSet( input, 'test', 1 );
+
+ expect( result ).not.toBe( input );
+ } );
+
+ it( 'clones the object at deeper levels', () => {
+ const input = { foo: { bar: { baz: 1 } } };
+ const result = immutableSet( input, [ 'foo', 'bar', 'baz' ], 2 );
+
+ expect( result ).not.toBe( input );
+ expect( result.foo ).not.toBe( input.foo );
+ expect( result.foo.bar ).not.toBe( input.foo.bar );
+ expect( result.foo.bar.baz ).not.toBe( input.foo.bar.baz );
+ } );
+ } );
+} );
+
describe( 'anchor', () => {
const blockSettings = {
save: noop,
diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js
index f8feba34fabece..855c065ec72f3a 100644
--- a/packages/block-editor/src/hooks/utils.js
+++ b/packages/block-editor/src/hooks/utils.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { isEmpty, mapValues, get, setWith, clone } from 'lodash';
+import { isEmpty, mapValues, get } from 'lodash';
/**
* WordPress dependencies
@@ -30,8 +30,74 @@ export const cleanEmptyObject = ( object ) => {
return isEmpty( cleanedNestedObjects ) ? undefined : cleanedNestedObjects;
};
+/**
+ * Converts a path to an array of its fragments.
+ * Supports strings, numbers and arrays:
+ *
+ * 'foo' => [ 'foo' ]
+ * 2 => [ '2' ]
+ * [ 'foo', 'bar' ] => [ 'foo', 'bar' ]
+ *
+ * @param {string|number|Array} path Path
+ * @return {Array} Normalized path.
+ */
+function normalizePath( path ) {
+ if ( Array.isArray( path ) ) {
+ return path;
+ } else if ( typeof path === 'number' ) {
+ return [ path.toString() ];
+ }
+
+ return [ path ];
+}
+
+/**
+ * Clones an object.
+ * Non-object values are returned unchanged.
+ *
+ * @param {*} object Object to clone.
+ * @return {*} Cloned object, or original literal non-object value.
+ */
+function cloneObject( object ) {
+ if ( typeof object === 'object' ) {
+ return {
+ ...Object.fromEntries(
+ Object.entries( object ).map( ( [ key, value ] ) => [
+ key,
+ cloneObject( value ),
+ ] )
+ ),
+ };
+ }
+
+ return object;
+}
+
+/**
+ * Perform an immutable set.
+ * Handles nullish initial values.
+ * Clones all nested objects in the specified object.
+ *
+ * @param {Object} object Object to set a value in.
+ * @param {number|string|Array} path Path in the object to modify.
+ * @param {*} value New value to set.
+ * @return {Object} Cloned object with the new value set.
+ */
export function immutableSet( object, path, value ) {
- return setWith( object ? clone( object ) : {}, path, value, clone );
+ const normalizedPath = normalizePath( path );
+ const newObject = object ? cloneObject( object ) : {};
+
+ normalizedPath.reduce( ( acc, key, i ) => {
+ if ( acc[ key ] === undefined ) {
+ acc[ key ] = {};
+ }
+ if ( i === normalizedPath.length - 1 ) {
+ acc[ key ] = value;
+ }
+ return acc[ key ];
+ }, newObject );
+
+ return newObject;
}
export function transformStyles(
diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js
index 1c81c910b21e12..d883aa455bc8f3 100644
--- a/packages/block-editor/src/index.js
+++ b/packages/block-editor/src/index.js
@@ -20,3 +20,4 @@ export * from './elements';
export * from './utils';
export { storeConfig, store } from './store';
export { SETTINGS_DEFAULTS } from './store/defaults';
+export { experiments } from './experiments';
diff --git a/packages/block-library/src/column/edit.native.js b/packages/block-library/src/column/edit.native.js
index b3fcb3f84d2cdc..e5ad6c7827d236 100644
--- a/packages/block-library/src/column/edit.native.js
+++ b/packages/block-library/src/column/edit.native.js
@@ -152,6 +152,11 @@ function ColumnEdit( {
);
}
+ const parentWidth =
+ contentStyle &&
+ contentStyle[ clientId ] &&
+ contentStyle[ clientId ].width;
+
return (
<>
{ isSelected && (
@@ -211,7 +216,7 @@ function ColumnEdit( {
>
diff --git a/packages/block-library/src/post-author-name/block.json b/packages/block-library/src/post-author-name/block.json
index 34cf8886e9f032..29ad4b0d4deb27 100644
--- a/packages/block-library/src/post-author-name/block.json
+++ b/packages/block-library/src/post-author-name/block.json
@@ -1,7 +1,6 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
- "__experimental": true,
"name": "core/post-author-name",
"title": "Post Author Name",
"category": "theme",
diff --git a/packages/block-library/src/post-template/edit.js b/packages/block-library/src/post-template/edit.js
index a6d082afab059e..1acb3e57191758 100644
--- a/packages/block-library/src/post-template/edit.js
+++ b/packages/block-library/src/post-template/edit.js
@@ -74,7 +74,7 @@ export default function PostTemplateEdit( {
context: {
query: {
perPage,
- offset,
+ offset = 0,
postType,
order,
orderBy,
diff --git a/packages/block-library/src/rss/edit.js b/packages/block-library/src/rss/edit.js
index f7689b8c38ca1c..0cf252e038b6f9 100644
--- a/packages/block-library/src/rss/edit.js
+++ b/packages/block-library/src/rss/edit.js
@@ -12,9 +12,10 @@ import {
PanelBody,
Placeholder,
RangeControl,
- TextControl,
ToggleControl,
ToolbarGroup,
+ __experimentalHStack as HStack,
+ __experimentalInputControl as InputControl,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import { grid, list, edit, rss } from '@wordpress/icons';
@@ -66,17 +67,20 @@ export default function RSSEdit( { attributes, setAttributes } ) {
onSubmit={ onSubmitURL }
className="wp-block-rss__placeholder-form"
>
-
- setAttributes( { feedURL: value } )
- }
- className="wp-block-rss__placeholder-input"
- />
-
+
+
+ setAttributes( { feedURL: value } )
+ }
+ className="wp-block-rss__placeholder-input"
+ />
+
+
diff --git a/packages/block-library/src/rss/editor.scss b/packages/block-library/src/rss/editor.scss
index aa6288897aea4b..9b4bac2874ad5b 100644
--- a/packages/block-library/src/rss/editor.scss
+++ b/packages/block-library/src/rss/editor.scss
@@ -3,9 +3,6 @@
}
.wp-block-rss__placeholder-form {
- display: flex;
- align-items: stretch;
-
> * {
margin-bottom: $grid-unit-10;
}
@@ -15,18 +12,10 @@
margin-bottom: 0;
}
}
-}
-.wp-block-rss__placeholder-input {
- display: flex;
- align-items: stretch;
- flex-grow: 1;
-
- .components-base-control__field {
- margin: 0;
- display: flex;
- align-items: stretch;
- flex-grow: 1;
- margin-right: $grid-unit-10;
+ .wp-block-rss__placeholder-input {
+ flex: 1;
+ min-width: 80%;
}
}
+
diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 9b2020f0eb1e27..5288baadc779ff 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -17,6 +17,7 @@
### Bug Fix
- `TabPanel`: Fix initial tab selection when the tab declaration is lazily added to the `tabs` array ([47100](https://github.com/WordPress/gutenberg/pull/47100)).
+- `InputControl`: Avoid the "controlled to uncontrolled" warning by forcing the internal `