+
) }
- { showMediaPanel && (
-
- ) }
{ showInserterHelpPanel && hoveredItem && (
) }
- { showPatternPanel && (
-
- ) }
);
}
diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss
index 580e4ec0e21f26..5e35180ae7c45c 100644
--- a/packages/block-editor/src/components/inserter/style.scss
+++ b/packages/block-editor/src/components/inserter/style.scss
@@ -62,7 +62,7 @@ $block-inserter-tabs-height: 44px;
.block-editor-inserter__popover .block-editor-inserter__menu {
margin: -$grid-unit-15;
- .block-editor-inserter__tabs div[role="tablist"] {
+ .block-editor-inserter__tablist {
top: $grid-unit-10 + $grid-unit-20 + $grid-unit-60 - $grid-unit-15;
}
@@ -115,7 +115,7 @@ $block-inserter-tabs-height: 44px;
flex-direction: column;
overflow: hidden;
- div[role="tablist"] {
+ .block-editor-inserter__tablist {
border-bottom: $border-width solid $gray-300;
button[role="tab"] {
@@ -130,7 +130,7 @@ $block-inserter-tabs-height: 44px;
}
}
- div[role="tabpanel"] {
+ .block-editor-inserter__tabpanel {
display: flex;
flex-grow: 1;
flex-direction: column;
@@ -228,7 +228,7 @@ $block-inserter-tabs-height: 44px;
display: block;
}
- .block-editor-block-preview__container {
+ .block-editor-inserter__media-list__list-item {
height: 100%;
}
@@ -239,63 +239,81 @@ $block-inserter-tabs-height: 44px;
}
}
-.block-editor-inserter__patterns-explore-button.components-button {
- padding: $grid-unit-20;
- justify-content: center;
- margin-top: $grid-unit-20;
- width: 100%;
-}
-
-.block-editor-inserter__patterns-selected-category.block-editor-inserter__patterns-selected-category {
- color: var(--wp-admin-theme-color);
- position: relative;
-
- .components-flex-item {
- filter: brightness(0.95);
- }
-
- svg {
- fill: var(--wp-admin-theme-color);
- }
-
- &::after {
- content: "";
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- border-radius: $radius-block-ui;
- opacity: 0.04;
- background: var(--wp-admin-theme-color);
- }
-}
-
+.block-editor-inserter__media-tabs-container,
.block-editor-inserter__block-patterns-tabs-container {
+ padding: $grid-unit-20;
height: 100%;
- nav {
- height: 100%;
- }
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
}
-.block-editor-inserter__block-patterns-tabs {
+.block-editor-inserter__category-tablist {
display: flex;
flex-direction: column;
- padding: $grid-unit-20;
- overflow-y: auto;
- height: 100%;
+ border: none;
+ margin-bottom: $grid-unit-10;
// Push the listitem wrapping the "explore" button to the bottom of the panel.
div[role="listitem"]:last-child {
margin-top: auto;
}
- .block-editor-inserter__patterns-category {
+ .block-editor-inserter__category-tab {
// Account for the icon on the right so that it's visually balanced.
- padding-right: $grid-unit-05;
+ padding: $grid-unit-10 $grid-unit-05 $grid-unit-10 $grid-unit-15;
+ text-align: left;
+ font-weight: inherit;
+ display: block;
+ position: relative;
+ height: auto;
+
+ &[aria-selected="true"] {
+ color: var(--wp-admin-theme-color);
+
+ .components-flex-item {
+ filter: brightness(0.95);
+ }
+
+ svg {
+ fill: var(--wp-admin-theme-color);
+ }
+
+ &::after {
+ content: "";
+ display: block;
+ outline: none;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-radius: $radius-block-ui;
+ opacity: 0.04;
+ background: var(--wp-admin-theme-color);
+ height: 100%;
+ }
+ }
+
+ &:focus-visible,
+ &:focus:not(:disabled) {
+ border-radius: 2px;
+ box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
+ // Windows high contrast mode.
+ outline: 2px solid transparent;
+ outline-offset: 0;
+ }
+
+ &::before {
+ display: none;
+ }
+
+ &::after {
+ display: none;
+ }
}
}
-.block-editor-inserter__patterns-category-dialog {
+.block-editor-inserter__category-panel {
background: $gray-100;
border-left: $border-width solid $gray-200;
border-right: $border-width solid $gray-200;
@@ -304,35 +322,30 @@ $block-inserter-tabs-height: 44px;
left: 0;
height: 100%;
width: 100%;
+ padding: 0 $grid-unit-20;
+ display: flex;
+ flex-direction: column;
@include break-medium {
+ padding: 0;
left: 100%;
- display: block;
width: 300px;
}
+}
- .block-editor-block-patterns-list {
- overflow-y: auto;
- flex-grow: 1;
- height: 100%;
- padding: $grid-unit-20 $grid-unit-30;
- }
+.block-editor-inserter__patterns-category-panel-header {
+ padding: 16px $grid-unit-30;
+}
+.block-editor-inserter__patterns-category-no-results {
+ margin-top: $grid-unit-30;
}
-.block-editor-inserter__patterns-category-panel {
- padding: 0 $grid-unit-20;
- display: flex;
- flex-direction: column;
+.block-editor-inserter__media-list,
+.block-editor-block-patterns-list {
+ overflow-y: auto;
+ flex-grow: 1;
height: 100%;
- @include break-medium {
- padding: 0;
- }
- .block-editor-inserter__patterns-category-panel-header {
- padding: 16px $grid-unit-30;
- }
- .block-editor-inserter__patterns-category-no-results {
- margin-top: $grid-unit-30;
- }
+ padding: $grid-unit-20 $grid-unit-30;
}
.block-editor-inserter__preview-content {
@@ -392,7 +405,7 @@ $block-inserter-tabs-height: 44px;
.block-editor-block-patterns-list__list-item {
margin-bottom: 0;
}
- .block-editor-block-preview__container {
+ .block-editor-inserter__media-list__list-item {
min-height: 100px;
}
}
@@ -485,7 +498,7 @@ $block-inserter-tabs-height: 44px;
min-height: 240px;
}
- .block-editor-block-preview__container {
+ .block-editor-inserter__media-list__list-item {
height: inherit;
min-height: 100px;
max-height: 800px;
@@ -497,14 +510,9 @@ $block-inserter-tabs-height: 44px;
font-size: calc(1.25 * 13px);
}
-.block-editor-inserter__media-tabs-container {
- height: 100%;
-
- nav {
- height: 100%;
- }
-
- .block-editor-inserter__media-library-button {
+.block-editor-inserter__patterns-explore-button,
+.block-editor-inserter__media-library-button {
+ &.components-button {
padding: $grid-unit-20;
justify-content: center;
margin-top: $grid-unit-20;
@@ -512,76 +520,6 @@ $block-inserter-tabs-height: 44px;
}
}
-.block-editor-inserter__media-tabs {
- display: flex;
- flex-direction: column;
- padding: $grid-unit-20;
- overflow-y: auto;
- height: 100%;
-
- // Push the listitem wrapping the "open media library" button to the bottom of the panel.
- div[role="listitem"]:last-child {
- margin-top: auto;
- }
-
- .block-editor-inserter__media-tabs__media-category {
- // Account for the icon on the right so that it's visually balanced.
- padding-right: $grid-unit-05;
-
- &.is-selected {
- color: var(--wp-admin-theme-color);
- position: relative;
-
- .components-flex-item {
- filter: brightness(0.95);
- }
-
- svg {
- fill: var(--wp-admin-theme-color);
- }
-
- &::after {
- content: "";
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- border-radius: $radius-block-ui;
- opacity: 0.04;
- background: var(--wp-admin-theme-color);
- }
- }
- }
-}
-
-.block-editor-inserter__media-dialog {
- background: $gray-100;
- border-left: $border-width solid $gray-200;
- border-right: $border-width solid $gray-200;
- position: absolute;
- padding: $grid-unit-20 $grid-unit-30;
- top: 0;
- left: 0;
- height: 100%;
- width: 100%;
- overflow-y: auto;
- scrollbar-gutter: stable both-edges;
-
- @include break-medium {
- left: 100%;
- display: block;
- width: 300px;
- }
-
- .block-editor-block-preview__container {
- box-shadow: 0 15px 25px rgb(0 0 0 / 7%);
- &:hover {
- box-shadow: 0 0 0 2px $gray-900, 0 15px 25px rgb(0 0 0 / 7%);
- }
- }
-}
-
.block-editor-inserter__media-panel {
min-height: 100%;
padding: 0 $grid-unit-20;
@@ -601,6 +539,7 @@ $block-inserter-tabs-height: 44px;
}
.block-editor-inserter__media-panel-search {
+ padding: $grid-unit-20 $grid-unit-30 0;
// TODO: Consider using the Theme component to automatically adapt to a gray background.
&:not(:focus-within) {
--wp-components-color-background: #{$white};
@@ -608,90 +547,88 @@ $block-inserter-tabs-height: 44px;
}
}
-.block-editor-inserter__media-list {
- margin-top: $grid-unit-20;
- .block-editor-inserter__media-list__list-item {
- position: relative;
- cursor: pointer;
- margin-bottom: $grid-unit-30;
+.block-editor-inserter__media-list__list-item {
+ position: relative;
+ cursor: pointer;
+ margin-bottom: $grid-unit-30;
+ box-shadow: 0 15px 25px rgb(0 0 0 / 7%);
- &.is-placeholder {
- min-height: 100px;
- }
+ &.is-placeholder {
+ min-height: 100px;
+ }
- &[draggable="true"] .block-editor-block-preview__container {
- cursor: grab;
+ &[draggable="true"] .block-editor-inserter__media-list__list-item {
+ cursor: grab;
+ }
+
+ &.is-hovered {
+ .block-editor-inserter__media-list__item-preview {
+ box-shadow: 0 0 0 2px $gray-900, 0 15px 25px rgb(0 0 0 / 7%);
}
- &.is-hovered {
- .block-editor-inserter__media-list__item-preview {
- box-shadow: 0 0 0 2px $gray-900, 0 15px 25px rgb(0 0 0 / 7%);
- }
+ .block-editor-inserter__media-list__item-preview-options > button {
+ display: block;
+ }
+ }
- .block-editor-inserter__media-list__item-preview-options > button {
+ .block-editor-inserter__media-list__item-preview-options {
+ position: absolute;
+ right: $grid-unit-10;
+ top: $grid-unit-10;
+
+ > button {
+ background: $white;
+ border-radius: $radius-block-ui;
+ display: none;
+
+ // These styles are important so as focus isn't lost
+ // when the button is focused and we hover away.
+ &.is-opened,
+ &:focus {
display: block;
}
- }
-
- .block-editor-inserter__media-list__item-preview-options {
- position: absolute;
- right: $grid-unit-10;
- top: $grid-unit-10;
- > button {
- background: $white;
- border-radius: $radius-block-ui;
- display: none;
-
- // These styles are important so as focus isn't lost
- // when the button is focused and we hover away.
- &.is-opened,
- &:focus {
- display: block;
- }
-
- &:hover {
- box-shadow: inset 0 0 0 2px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
+ &:hover {
+ box-shadow: inset 0 0 0 2px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
- // Windows High Contrast mode will show this outline, but not the box-shadow.
- outline: 2px solid transparent;
- }
+ // Windows High Contrast mode will show this outline, but not the box-shadow.
+ outline: 2px solid transparent;
}
}
}
+}
- .block-editor-inserter__media-list__item {
- height: 100%;
+.block-editor-inserter__media-list__item {
+ height: 100%;
- .block-editor-inserter__media-list__item-preview {
- display: flex;
- align-items: center;
- overflow: hidden;
- border-radius: 2px;
+ .block-editor-inserter__media-list__item-preview {
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ border-radius: 2px;
- > * {
- margin: 0 auto;
- max-width: 100%;
- }
+ > * {
+ margin: 0 auto;
+ max-width: 100%;
+ }
- .block-editor-inserter__media-list__item-preview-spinner {
- display: flex;
- height: 100%;
- width: 100%;
- position: absolute;
- justify-content: center;
- background: rgba($white, 0.7);
- align-items: center;
- pointer-events: none;
- }
+ .block-editor-inserter__media-list__item-preview-spinner {
+ display: flex;
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ justify-content: center;
+ background: rgba($white, 0.7);
+ align-items: center;
+ pointer-events: none;
}
+ }
- &:focus .block-editor-inserter__media-list__item-preview {
- box-shadow: inset 0 0 0 2px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
+ &:focus .block-editor-inserter__media-list__item-preview {
+ box-shadow: inset 0 0 0 2px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
- // Windows High Contrast mode will show this outline, but not the box-shadow.
- outline: 2px solid transparent;
- }
+ // Windows High Contrast mode will show this outline, but not the box-shadow.
+ outline: 2px solid transparent;
}
}
@@ -758,16 +695,23 @@ $block-inserter-tabs-height: 44px;
}
}
+// Only relevant in zoom-out-mode
+.block-editor-inserter__pattern-panel-placeholder {
+ display: none;
+}
+
.is-zoom-out {
.block-editor-inserter__menu {
display: flex;
}
- .block-editor-inserter__patterns-category-dialog {
- position: static;
- }
-
- .block-editor-inserter__media-dialog {
- position: static;
+ .show-panel::after {
+ // Makes space for the inserter flyout panel
+ @include break-medium {
+ content: "";
+ display: block;
+ width: 300px;
+ height: 100%;
+ }
}
}
diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js
index 4795c3ce4fdc24..ad9cd4888bd947 100644
--- a/packages/block-editor/src/components/inserter/tabs.js
+++ b/packages/block-editor/src/components/inserter/tabs.js
@@ -43,9 +43,13 @@ function InserterTabs( {
return (
-
+
{ tabs.map( ( tab ) => (
-
+
{ tab.title }
) ) }
@@ -55,6 +59,7 @@ function InserterTabs( {
key={ tab.name }
tabId={ tab.name }
focusable={ false }
+ className="block-editor-inserter__tabpanel"
>
{ tabsContents[ tab.name ] }
diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js
index 930d0a0f80ef66..658188dbd0fc70 100644
--- a/packages/block-editor/src/components/list-view/block.js
+++ b/packages/block-editor/src/components/list-view/block.js
@@ -82,25 +82,26 @@ function ListViewBlock( {
const blockInformation = useBlockDisplayInformation( clientId );
- const { block, blockName, blockEditingMode } = useSelect(
- ( select ) => {
- const { getBlock, getBlockName, getBlockEditingMode } =
- select( blockEditorStore );
-
- return {
- block: getBlock( clientId ),
- blockName: getBlockName( clientId ),
- blockEditingMode: getBlockEditingMode( clientId ),
- };
- },
- [ clientId ]
- );
-
- const allowRightClickOverrides = useSelect(
- ( select ) =>
- select( blockEditorStore ).getSettings().allowRightClickOverrides,
- []
- );
+ const { block, blockName, blockEditingMode, allowRightClickOverrides } =
+ useSelect(
+ ( select ) => {
+ const {
+ getBlock,
+ getBlockName,
+ getBlockEditingMode,
+ getSettings,
+ } = select( blockEditorStore );
+
+ return {
+ block: getBlock( clientId ),
+ blockName: getBlockName( clientId ),
+ blockEditingMode: getBlockEditingMode( clientId ),
+ allowRightClickOverrides:
+ getSettings().allowRightClickOverrides,
+ };
+ },
+ [ clientId ]
+ );
const showBlockActions =
// When a block hides its toolbar it also hides the block settings menu,
diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js
index 8a696c6f56c241..7ef5d270f50a27 100644
--- a/packages/block-editor/src/components/list-view/index.js
+++ b/packages/block-editor/src/components/list-view/index.js
@@ -64,7 +64,7 @@ const expanded = ( state, action ) => {
return state;
};
-export const BLOCK_LIST_ITEM_HEIGHT = 36;
+export const BLOCK_LIST_ITEM_HEIGHT = 32;
/** @typedef {import('react').ComponentType} ComponentType */
/** @typedef {import('react').Ref} Ref */
diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss
index 7a9abb7a6b4814..ea3cc5bb78cdf3 100644
--- a/packages/block-editor/src/components/list-view/style.scss
+++ b/packages/block-editor/src/components/list-view/style.scss
@@ -156,13 +156,13 @@
&.is-displacement-up {
transition: transform 0.2s;
- transform: translateY(-36px);
+ transform: translateY(-32px);
@include reduce-motion("transition");
}
&.is-displacement-down {
transition: transform 0.2s;
- transform: translateY(36px);
+ transform: translateY(32px);
@include reduce-motion("transition");
}
@@ -172,20 +172,20 @@
// worth of space for the visual indicator of where a block will be placed when dropped.
&.is-after-dragged-blocks {
transition: transform 0.2s;
- transform: translateY(calc(var(--wp-admin--list-view-dragged-items-height, 36px) * -1));
+ transform: translateY(calc(var(--wp-admin--list-view-dragged-items-height, 32px) * -1));
@include reduce-motion("transition");
}
&.is-after-dragged-blocks.is-displacement-up {
transition: transform 0.2s;
- transform: translateY(calc(-36px + var(--wp-admin--list-view-dragged-items-height, 36px) * -1));
+ transform: translateY(calc(-32px + var(--wp-admin--list-view-dragged-items-height, 32px) * -1));
@include reduce-motion("transition");
}
&.is-after-dragged-blocks.is-displacement-down {
transition: transform 0.2s;
transform:
- translateY(calc(36px + var(--wp-admin--list-view-dragged-items-height, 36px) *
+ translateY(calc(32px + var(--wp-admin--list-view-dragged-items-height, 32px) *
-1));
@include reduce-motion("transition");
}
@@ -204,15 +204,15 @@
z-index: -9999;
}
- // List View renders a fixed number of items and relies on each item having a fixed height of 36px.
+ // List View renders a fixed number of items and relies on each item having a fixed height of 32px.
// If this value changes, we should also change the itemHeight value set in useFixedWindowList.
// See: https://github.com/WordPress/gutenberg/pull/35230 for additional context.
.block-editor-list-view-block-contents {
display: flex;
align-items: center;
width: 100%;
- height: auto;
- padding: ($grid-unit-15 * 0.5) ($grid-unit-15 * 0.5) ($grid-unit-15 * 0.5) 0;
+ height: $grid-unit-40;
+ padding: ($grid-unit-15 * 0.5) $grid-unit-05 ($grid-unit-15 * 0.5) 0;
text-align: left;
border-radius: $radius-block-ui;
position: relative;
@@ -277,15 +277,14 @@
}
.block-editor-block-icon {
- margin-right: $grid-unit-10;
+ margin-right: $grid-unit-10 * 0.5; // 6px.
flex: 0 0 $icon-size;
}
.block-editor-list-view-block__menu-cell,
.block-editor-list-view-block__mover-cell,
.block-editor-list-view-block__contents-cell {
- padding-top: 0;
- padding-bottom: 0;
+ padding: 0;
}
.block-editor-list-view-block__menu-cell,
@@ -316,7 +315,7 @@
}
.block-editor-list-view-block__menu-cell {
- padding-right: $grid-unit-15 * 0.5; // 6px.
+ padding-right: $grid-unit-05;
.components-button.has-icon {
height: 24px;
@@ -379,8 +378,10 @@
}
}
- .block-editor-list-view-block-select-button__label-wrapper {
- min-width: 120px;
+ // Style lock and position icons in line with image previews.
+ .block-editor-list-view-block-select-button__label-wrapper svg {
+ left: $grid-unit-05 * 0.5; // 2px.
+ position: relative;
}
.block-editor-list-view-block-select-button__title {
@@ -464,21 +465,12 @@ $block-navigation-max-indent: 8;
// Chevron container metrics.
.block-editor-list-view__expander {
height: $icon-size;
- margin-left: $grid-unit-05;
width: $icon-size;
cursor: pointer;
}
.block-editor-list-view-leaf[aria-level] .block-editor-list-view__expander {
- margin-left:
- ($icon-size) * $block-navigation-max-indent + 4 *
- ($block-navigation-max-indent - 1);
-}
-
-.block-editor-list-view-leaf:not([aria-level="1"]) {
- .block-editor-list-view__expander {
- margin-right: 4px;
- }
+ margin-left: ($grid-unit-30 * $block-navigation-max-indent);
}
// When updating the margin for each indentation level, the corresponding
@@ -488,9 +480,9 @@ $block-navigation-max-indent: 8;
.block-editor-list-view-leaf[aria-level="#{ $i + 1 }"]
.block-editor-list-view__expander {
@if $i - 1 >= 0 {
- margin-left: ($icon-size * $i) + 4 * ($i - 1);
+ margin-left: ($grid-unit-30 * $i); // Effectivly centers the expander below the parent's icon.
} @else {
- margin-left: ($icon-size * $i);
+ margin-left: 0;
}
}
}
@@ -540,7 +532,7 @@ svg {
.block-editor-list-view-drop-indicator__line {
background: rgba(var(--wp-admin-theme-color--rgb), 0.04);
- height: 36px;
+ height: 32px;
border-radius: 4px;
overflow: hidden;
}
@@ -553,7 +545,7 @@ svg {
.block-editor-list-view-placeholder {
padding: 0;
margin: 0;
- height: 36px;
+ height: 32px;
}
.list-view-appender .block-editor-inserter__toggle {
diff --git a/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js b/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js
index 3354b3f41d391c..64730be6156df9 100644
--- a/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js
+++ b/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js
@@ -62,7 +62,7 @@ import { store as blockEditorStore } from '../../store';
// When the indentation level, the corresponding left margin in `style.scss`
// must be updated as well to ensure the drop zone is aligned with the indentation.
-export const NESTING_LEVEL_INDENTATION = 28;
+export const NESTING_LEVEL_INDENTATION = 24;
/**
* Determines whether the user is positioning the dragged block to be
@@ -556,9 +556,14 @@ export default function useListViewDropZone( {
const ref = useDropZone( {
dropZoneElement,
onDrop( event ) {
+ throttled.cancel();
if ( target ) {
onBlockDrop( event );
}
+ // Use `undefined` value to indicate that the drag has concluded.
+ // This allows styling rules that are active only when a user is
+ // dragging to be removed.
+ setTarget( undefined );
},
onDragLeave() {
throttled.cancel();
diff --git a/packages/block-editor/src/components/provider/test/use-block-sync.js b/packages/block-editor/src/components/provider/test/use-block-sync.js
index 9b16e966249fa7..aae5e517c63029 100644
--- a/packages/block-editor/src/components/provider/test/use-block-sync.js
+++ b/packages/block-editor/src/components/provider/test/use-block-sync.js
@@ -71,6 +71,7 @@ describe( 'useBlockSync hook', () => {
expect( onInput ).not.toHaveBeenCalled();
expect( replaceInnerBlocks ).not.toHaveBeenCalled();
expect( resetBlocks ).toHaveBeenCalledWith( fakeBlocks );
+ expect( resetBlocks ).toHaveBeenCalledTimes( 1 );
const testBlocks = [
{ clientId: 'a', innerBlocks: [], attributes: { foo: 1 } },
@@ -88,6 +89,7 @@ describe( 'useBlockSync hook', () => {
expect( onInput ).not.toHaveBeenCalled();
expect( replaceInnerBlocks ).not.toHaveBeenCalled();
expect( resetBlocks ).toHaveBeenCalledWith( testBlocks );
+ expect( resetBlocks ).toHaveBeenCalledTimes( 2 );
unmount();
@@ -95,6 +97,7 @@ describe( 'useBlockSync hook', () => {
expect( onInput ).not.toHaveBeenCalled();
expect( replaceInnerBlocks ).not.toHaveBeenCalled();
expect( resetBlocks ).toHaveBeenCalledWith( [] );
+ expect( resetBlocks ).toHaveBeenCalledTimes( 3 );
} );
it( 'replaces the inner blocks of a block when the controlled value changes if a clientId is passed', async () => {
@@ -123,6 +126,7 @@ describe( 'useBlockSync hook', () => {
'test', // It should use the given client ID.
fakeBlocks // It should use the controlled blocks value.
);
+ expect( replaceInnerBlocks ).toHaveBeenCalledTimes( 1 );
const testBlocks = [
{
@@ -148,6 +152,7 @@ describe( 'useBlockSync hook', () => {
expect( replaceInnerBlocks ).toHaveBeenCalledWith( 'test', [
expect.objectContaining( { name: 'test/test-block' } ),
] );
+ expect( replaceInnerBlocks ).toHaveBeenCalledTimes( 2 );
unmount();
@@ -155,6 +160,7 @@ describe( 'useBlockSync hook', () => {
expect( onInput ).not.toHaveBeenCalled();
expect( resetBlocks ).not.toHaveBeenCalled();
expect( replaceInnerBlocks ).toHaveBeenCalledWith( 'test', [] );
+ expect( replaceInnerBlocks ).toHaveBeenCalledTimes( 3 );
} );
it( 'does not add the controlled blocks to the block-editor store if the store already contains them', async () => {
@@ -354,6 +360,7 @@ describe( 'useBlockSync hook', () => {
);
expect( replaceInnerBlocks ).toHaveBeenCalledWith( 'test', [] );
+ expect( replaceInnerBlocks ).toHaveBeenCalledTimes( 1 );
expect( onChange ).not.toHaveBeenCalled();
expect( onInput ).not.toHaveBeenCalled();
} );
diff --git a/packages/block-editor/src/components/provider/use-block-sync.js b/packages/block-editor/src/components/provider/use-block-sync.js
index 969c0f1e4d1c5e..7dfbd6a8f320e9 100644
--- a/packages/block-editor/src/components/provider/use-block-sync.js
+++ b/packages/block-editor/src/components/provider/use-block-sync.js
@@ -186,7 +186,15 @@ export default function useBlockSync( {
}
}, [ controlledBlocks, clientId ] );
+ const isMounted = useRef( false );
+
useEffect( () => {
+ // On mount, controlled blocks are already set in the effect above.
+ if ( ! isMounted.current ) {
+ isMounted.current = true;
+ return;
+ }
+
// When the block becomes uncontrolled, it means its inner state has been reset
// we need to take the blocks again from the external value property.
if ( ! isControlled ) {
diff --git a/packages/block-editor/src/components/rich-text/README.md b/packages/block-editor/src/components/rich-text/README.md
index d6469bf098145b..a4c9b932e87905 100644
--- a/packages/block-editor/src/components/rich-text/README.md
+++ b/packages/block-editor/src/components/rich-text/README.md
@@ -25,6 +25,10 @@ _Default: `div`._ The [tag name](https://www.w3.org/TR/html51/syntax.html#tag-na
_Optional._ Placeholder text to show when the field is empty, similar to the
[`input` and `textarea` attribute of the same name](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/HTML5_updates#The_placeholder_attribute).
+### `disableLineBreaks: Boolean`
+
+_Optional._ Disables inserting line breaks on `Enter` when it is set to `true`
+
### `multiline: Boolean | String`
_Optional._ By default, a line break will be inserted on Enter. If the editable field can contain multiple paragraphs, this property can be set to create new paragraphs on Enter.
diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js
index 8371460862fa55..5729aa2d4bf27f 100644
--- a/packages/block-editor/src/components/rich-text/index.native.js
+++ b/packages/block-editor/src/components/rich-text/index.native.js
@@ -118,6 +118,7 @@ export function RichTextWrapper(
getBlock,
isMultiSelecting,
hasMultiSelection,
+ getSelectedBlockClientId,
} = select( blockEditorStore );
const selectionStart = getSelectionStart();
@@ -154,6 +155,7 @@ export function RichTextWrapper(
didAutomaticChange: didAutomaticChange(),
disabled: isMultiSelecting() || hasMultiSelection(),
undo,
+ getSelectedBlockClientId,
...extraProps,
};
};
@@ -164,6 +166,7 @@ export function RichTextWrapper(
selectionStart,
selectionEnd,
isSelected,
+ getSelectedBlockClientId,
didAutomaticChange,
disabled,
undo,
@@ -175,6 +178,7 @@ export function RichTextWrapper(
exitFormattedText,
selectionChange,
__unstableMarkAutomaticChange,
+ clearSelectedBlock,
} = useDispatch( blockEditorStore );
const adjustedAllowedFormats = getAllowedFormats( {
allowedFormats,
@@ -209,6 +213,12 @@ export function RichTextWrapper(
[ clientId, identifier ]
);
+ const clearCurrentSelectionOnUnmount = useCallback( () => {
+ if ( getSelectedBlockClientId() === clientId ) {
+ clearSelectedBlock();
+ }
+ }, [ clearSelectedBlock, clientId, getSelectedBlockClientId ] );
+
const onDelete = useCallback(
( { value, isReverse } ) => {
if ( onMerge ) {
@@ -590,6 +600,7 @@ export function RichTextWrapper(
disableSuggestions={ disableSuggestions }
disableAutocorrection={ disableAutocorrection }
containerWidth={ containerWidth }
+ clearCurrentSelectionOnUnmount={ clearCurrentSelectionOnUnmount }
// Props to be set on the editable container are destructured on the
// element itself for web (see below), but passed through rich text
// for native.
diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js
index 8b4c871bda7bbf..26d39a0c6058b4 100644
--- a/packages/block-editor/src/components/rich-text/native/index.native.js
+++ b/packages/block-editor/src/components/rich-text/native/index.native.js
@@ -873,6 +873,17 @@ export class RichText extends Component {
}
}
+ componentWillUnmount() {
+ const { clearCurrentSelectionOnUnmount } = this.props;
+
+ // There are cases when the component is unmounted e.g. scrolling in a
+ // long post due to virtualization, so the block selection needs to be cleared
+ // so it doesn't auto-focus when it's added back.
+ if ( this._editor?.isFocused() ) {
+ clearCurrentSelectionOnUnmount?.();
+ }
+ }
+
getHtmlToRender( record, tagName ) {
// Save back to HTML from React tree.
let value = this.valueToFormat( record );
diff --git a/packages/block-editor/src/components/rich-text/use-undo-automatic-change.js b/packages/block-editor/src/components/rich-text/use-undo-automatic-change.js
index 819f2935806263..9094633186ebdd 100644
--- a/packages/block-editor/src/components/rich-text/use-undo-automatic-change.js
+++ b/packages/block-editor/src/components/rich-text/use-undo-automatic-change.js
@@ -3,7 +3,7 @@
*/
import { useSelect } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';
-import { BACKSPACE, DELETE, ESCAPE } from '@wordpress/keycodes';
+import { BACKSPACE, ESCAPE } from '@wordpress/keycodes';
/**
* Internal dependencies
@@ -20,11 +20,7 @@ export function useUndoAutomaticChange() {
return;
}
- if (
- keyCode !== DELETE &&
- keyCode !== BACKSPACE &&
- keyCode !== ESCAPE
- ) {
+ if ( keyCode !== BACKSPACE && keyCode !== ESCAPE ) {
return;
}
diff --git a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js
index 3a549bd8e15cc3..6feb583c29cdb2 100644
--- a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js
+++ b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js
@@ -92,6 +92,8 @@ export default function SpacingInputControl( {
! isValueSpacingPreset( value )
);
+ const [ minValue, setMinValue ] = useState( minimumCustomValue );
+
const previousValue = usePrevious( value );
if (
!! value &&
@@ -222,13 +224,26 @@ export default function SpacingInputControl( {
}
value={ currentValue }
units={ units }
- min={ minimumCustomValue }
+ min={ minValue }
placeholder={ allPlaceholder }
disableUnits={ isMixed }
label={ ariaLabel }
hideLabelFromVision
className="spacing-sizes-control__custom-value-input"
size={ '__unstable-large' }
+ onDragStart={ () => {
+ if ( value?.charAt( 0 ) === '-' ) {
+ setMinValue( 0 );
+ }
+ } }
+ onDrag={ () => {
+ if ( value?.charAt( 0 ) === '-' ) {
+ setMinValue( 0 );
+ }
+ } }
+ onDragEnd={ () => {
+ setMinValue( minimumCustomValue );
+ } }
/>
{
const {
- __unstableGetContentLockingParent,
+ getContentLockingParent,
getTemplateLock,
- __unstableGetTemporarilyEditingAsBlocks,
- } = select( blockEditorStore );
+ getTemporarilyEditingAsBlocks,
+ } = unlock( select( blockEditorStore ) );
return {
templateLock: getTemplateLock( clientId ),
- isLockedByParent:
- !! __unstableGetContentLockingParent( clientId ),
- isEditingAsBlocks:
- __unstableGetTemporarilyEditingAsBlocks() === clientId,
+ isLockedByParent: !! getContentLockingParent( clientId ),
+ isEditingAsBlocks: getTemporarilyEditingAsBlocks() === clientId,
};
},
[ clientId ]
diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js
index 5f07c9e843f4bf..ac6a381742c814 100644
--- a/packages/block-editor/src/hooks/layout.js
+++ b/packages/block-editor/src/hooks/layout.js
@@ -374,7 +374,7 @@ function BlockWithLayoutStyles( {
const { kebabCase } = unlock( componentsPrivateApis );
const selectorPrefix = `wp-container-${ kebabCase( name ) }-is-layout-`;
// Higher specificity to override defaults from theme.json.
- const selector = `.${ selectorPrefix }${ id }.${ selectorPrefix }${ id }`;
+ const selector = `.${ selectorPrefix }${ id }`;
const hasBlockGapSupport = blockGapSupport !== null;
// Get CSS string for the current layout type.
diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js
index 5cd8cb46b3b7e7..5ca526b5749b74 100644
--- a/packages/block-editor/src/hooks/use-bindings-attributes.js
+++ b/packages/block-editor/src/hooks/use-bindings-attributes.js
@@ -1,12 +1,11 @@
/**
* WordPress dependencies
*/
-import { getBlockType, store as blocksStore } from '@wordpress/blocks';
+import { store as blocksStore } from '@wordpress/blocks';
import { createHigherOrderComponent } from '@wordpress/compose';
-import { useSelect } from '@wordpress/data';
-import { useLayoutEffect, useCallback, useState } from '@wordpress/element';
+import { useRegistry, useSelect } from '@wordpress/data';
+import { useCallback } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
-import { RichTextData } from '@wordpress/rich-text';
/**
* Internal dependencies
@@ -56,181 +55,111 @@ export function canBindAttribute( blockName, attributeName ) {
);
}
-/**
- * This component is responsible for detecting and
- * propagating data changes from the source to the block.
- *
- * @param {Object} props - The component props.
- * @param {string} props.attrName - The attribute name.
- * @param {Object} props.blockProps - The block props with bound attribute.
- * @param {Object} props.source - Source handler.
- * @param {Object} props.args - The arguments to pass to the source.
- * @param {Function} props.onPropValueChange - The function to call when the attribute value changes.
- * @return {null} Data-handling component. Render nothing.
- */
-const BindingConnector = ( {
- args,
- attrName,
- blockProps,
- source,
- onPropValueChange,
-} ) => {
- const { placeholder, value: propValue } = source.useSource(
- blockProps,
- args
- );
-
- const { name: blockName } = blockProps;
- const attrValue = blockProps.attributes[ attrName ];
-
- const updateBoundAttibute = useCallback(
- ( newAttrValue, prevAttrValue ) => {
- /*
- * If the attribute is a RichTextData instance,
- * (core/paragraph, core/heading, core/button, etc.)
- * compare its HTML representation with the new value.
- *
- * To do: it looks like a workaround.
- * Consider improving the attribute and metadata fields types.
- */
- if ( prevAttrValue instanceof RichTextData ) {
- // Bail early if the Rich Text value is the same.
- if ( prevAttrValue.toHTMLString() === newAttrValue ) {
- return;
- }
-
- /*
- * To preserve the value type,
- * convert the new value to a RichTextData instance.
- */
- newAttrValue = RichTextData.fromHTMLString( newAttrValue );
- }
-
- if ( prevAttrValue === newAttrValue ) {
+export const withBlockBindingSupport = createHigherOrderComponent(
+ ( BlockEdit ) => ( props ) => {
+ const registry = useRegistry();
+ const sources = useSelect( ( select ) =>
+ unlock( select( blocksStore ) ).getAllBlockBindingsSources()
+ );
+ const bindings = props.attributes.metadata?.bindings;
+ const { name, clientId, context } = props;
+ const boundAttributes = useSelect( () => {
+ if ( ! bindings ) {
return;
}
- onPropValueChange( { [ attrName ]: newAttrValue } );
- },
- [ attrName, onPropValueChange ]
- );
-
- useLayoutEffect( () => {
- if ( typeof propValue !== 'undefined' ) {
- updateBoundAttibute( propValue, attrValue );
- } else if ( placeholder ) {
- /*
- * Placeholder fallback.
- * If the attribute is `src` or `href`,
- * a placeholder can't be used because it is not a valid url.
- * Adding this workaround until
- * attributes and metadata fields types are improved and include `url`.
- */
- const htmlAttribute =
- getBlockType( blockName ).attributes[ attrName ].attribute;
+ const attributes = {};
+
+ for ( const [ attributeName, boundAttribute ] of Object.entries(
+ bindings
+ ) ) {
+ const source = sources[ boundAttribute.source ];
+ if (
+ ! source?.getValue ||
+ ! canBindAttribute( name, attributeName )
+ ) {
+ continue;
+ }
- if ( htmlAttribute === 'src' || htmlAttribute === 'href' ) {
- updateBoundAttibute( null );
- return;
+ const args = {
+ registry,
+ context,
+ clientId,
+ attributeName,
+ args: boundAttribute.args,
+ };
+
+ attributes[ attributeName ] = source.getValue( args );
+
+ if ( attributes[ attributeName ] === undefined ) {
+ if ( attributeName === 'url' ) {
+ attributes[ attributeName ] = null;
+ } else {
+ attributes[ attributeName ] =
+ source.getPlaceholder?.( args );
+ }
+ }
}
- updateBoundAttibute( placeholder );
- }
- }, [
- updateBoundAttibute,
- propValue,
- attrValue,
- placeholder,
- blockName,
- attrName,
- ] );
-
- return null;
-};
+ return attributes;
+ }, [ bindings, name, clientId, context, registry, sources ] );
-/**
- * BlockBindingBridge acts like a component wrapper
- * that connects the bound attributes of a block
- * to the source handlers.
- * For this, it creates a BindingConnector for each bound attribute.
- *
- * @param {Object} props - The component props.
- * @param {Object} props.blockProps - The BlockEdit props object.
- * @param {Object} props.bindings - The block bindings settings.
- * @param {Function} props.onPropValueChange - The function to call when the attribute value changes.
- * @return {null} Data-handling component. Render nothing.
- */
-function BlockBindingBridge( { blockProps, bindings, onPropValueChange } ) {
- const blockBindingsSources = unlock(
- useSelect( blocksStore )
- ).getAllBlockBindingsSources();
+ const { setAttributes } = props;
- return (
- <>
- { Object.entries( bindings ).map(
- ( [ attrName, boundAttribute ] ) => {
- // Bail early if the block doesn't have a valid source handler.
- const source =
- blockBindingsSources[ boundAttribute.source ];
- if ( ! source?.useSource ) {
- return null;
+ const _setAttributes = useCallback(
+ ( nextAttributes ) => {
+ registry.batch( () => {
+ if ( ! bindings ) {
+ return setAttributes( nextAttributes );
}
- return (
-
- );
- }
- ) }
- >
- );
-}
-
-const withBlockBindingSupport = createHigherOrderComponent(
- ( BlockEdit ) => ( props ) => {
- /*
- * Collect and update the bound attributes
- * in a separate state.
- */
- const [ boundAttributes, setBoundAttributes ] = useState( {} );
- const updateBoundAttributes = useCallback(
- ( newAttributes ) =>
- setBoundAttributes( ( prev ) => ( {
- ...prev,
- ...newAttributes,
- } ) ),
- []
- );
+ const keptAttributes = { ...nextAttributes };
+
+ for ( const [
+ attributeName,
+ boundAttribute,
+ ] of Object.entries( bindings ) ) {
+ const source = sources[ boundAttribute.source ];
+ if (
+ ! source?.setValue ||
+ ! canBindAttribute( name, attributeName )
+ ) {
+ continue;
+ }
+
+ source.setValue( {
+ registry,
+ context,
+ clientId,
+ attributeName,
+ args: boundAttribute.args,
+ value: nextAttributes[ attributeName ],
+ } );
+ delete keptAttributes[ attributeName ];
+ }
- /*
- * Create binding object filtering
- * only the attributes that can be bound.
- */
- const bindings = Object.fromEntries(
- Object.entries( props.attributes.metadata?.bindings || {} ).filter(
- ( [ attrName ] ) => canBindAttribute( props.name, attrName )
- )
+ if ( Object.keys( keptAttributes ).length ) {
+ setAttributes( keptAttributes );
+ }
+ } );
+ },
+ [
+ registry,
+ bindings,
+ name,
+ clientId,
+ context,
+ setAttributes,
+ sources,
+ ]
);
return (
<>
- { Object.keys( bindings ).length > 0 && (
-
- ) }
-
>
);
diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js
index 84603c0161dd43..ce20cb5bd7a179 100644
--- a/packages/block-editor/src/hooks/use-zoom-out.js
+++ b/packages/block-editor/src/hooks/use-zoom-out.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
-import { useEffect } from '@wordpress/element';
+import { useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
@@ -11,26 +11,36 @@ import { store as blockEditorStore } from '../store';
/**
* A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode.
+ *
+ * @param {boolean} zoomOut If we should enter into zoomOut mode or not
*/
-export function useZoomOut() {
+export function useZoomOut( zoomOut = true ) {
const { __unstableSetEditorMode } = useDispatch( blockEditorStore );
- const { mode } = useSelect( ( select ) => {
- return {
- mode: select( blockEditorStore ).__unstableGetEditorMode(),
+ const { __unstableGetEditorMode } = useSelect( blockEditorStore );
+
+ const originalEditingMode = useRef( null );
+ const mode = __unstableGetEditorMode();
+
+ useEffect( () => {
+ // Only set this on mount so we know what to return to when we unmount.
+ if ( ! originalEditingMode.current ) {
+ originalEditingMode.current = mode;
+ }
+
+ return () => {
+ // We need to use __unstableGetEditorMode() here and not `mode`, as mode may not update on unmount
+ if ( __unstableGetEditorMode() !== originalEditingMode.current ) {
+ __unstableSetEditorMode( originalEditingMode.current );
+ }
};
}, [] );
- // Intentionality left without any dependency.
- // This effect should only run when the component is rendered and unmounted.
- // The effect opens the zoom-out view if it is not open before when applying a style variation.
+ // The effect opens the zoom-out view if we want it open and it's not currently in zoom-out mode.
useEffect( () => {
- if ( mode !== 'zoom-out' ) {
+ if ( zoomOut && mode !== 'zoom-out' ) {
__unstableSetEditorMode( 'zoom-out' );
- return () => {
- // Revert to original mode
- __unstableSetEditorMode( mode );
- };
+ } else if ( ! zoomOut && originalEditingMode.current !== mode ) {
+ __unstableSetEditorMode( originalEditingMode.current );
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [] );
+ }, [ __unstableSetEditorMode, zoomOut, mode ] );
}
diff --git a/packages/block-editor/src/layouts/definitions.js b/packages/block-editor/src/layouts/definitions.js
index 3b1e5c7ab5896a..a5c6d431c50f20 100644
--- a/packages/block-editor/src/layouts/definitions.js
+++ b/packages/block-editor/src/layouts/definitions.js
@@ -34,13 +34,13 @@ export const LAYOUT_DEFINITIONS = {
],
spacingStyles: [
{
- selector: ' > :first-child:first-child',
+ selector: ' > :first-child',
rules: {
'margin-block-start': '0',
},
},
{
- selector: ' > :last-child:last-child',
+ selector: ' > :last-child',
rules: {
'margin-block-end': '0',
},
@@ -100,13 +100,13 @@ export const LAYOUT_DEFINITIONS = {
],
spacingStyles: [
{
- selector: ' > :first-child:first-child',
+ selector: ' > :first-child',
rules: {
'margin-block-start': '0',
},
},
{
- selector: ' > :last-child:last-child',
+ selector: ' > :last-child',
rules: {
'margin-block-end': '0',
},
@@ -134,7 +134,7 @@ export const LAYOUT_DEFINITIONS = {
},
},
{
- selector: ' > *',
+ selector: ' > :is(*, div)', // :is(*, div) instead of just * increases the specificity by 001.
rules: {
margin: '0',
},
@@ -156,7 +156,7 @@ export const LAYOUT_DEFINITIONS = {
displayMode: 'grid',
baseStyles: [
{
- selector: ' > *',
+ selector: ' > :is(*, div)', // :is(*, div) instead of just * increases the specificity by 001.
rules: {
margin: '0',
},
diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js
index b51c019e791788..e81a5eed39d5bc 100644
--- a/packages/block-editor/src/private-apis.js
+++ b/packages/block-editor/src/private-apis.js
@@ -15,7 +15,7 @@ import {
} from './components/inserter/search-items';
import { PrivateListView } from './components/list-view';
import BlockInfo from './components/block-info-slot-fill';
-import { useShowBlockTools } from './components/block-tools/use-show-block-tools';
+import { useHasBlockToolbar } from './components/block-toolbar/use-has-block-toolbar';
import { cleanEmptyObject, useStyleOverride } from './hooks/utils';
import BlockQuickNavigation from './components/block-quick-navigation';
import { LayoutStyle } from './components/block-list/layout';
@@ -56,7 +56,7 @@ lock( privateApis, {
PrivateListView,
ResizableBoxPopover,
BlockInfo,
- useShowBlockTools,
+ useHasBlockToolbar,
cleanEmptyObject,
useStyleOverride,
BlockQuickNavigation,
diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js
index f98e4845e91f2f..faa36a286e046a 100644
--- a/packages/block-editor/src/store/actions.js
+++ b/packages/block-editor/src/store/actions.js
@@ -1438,26 +1438,42 @@ export const __unstableSetEditorMode =
// When switching to zoom-out mode, we need to select the parent section
if ( mode === 'zoom-out' ) {
const firstSelectedClientId = select.getBlockSelectionStart();
+ const allBlocks = select.getBlocks();
+
const { sectionRootClientId } = unlock(
registry.select( STORE_NAME ).getSettings()
);
if ( sectionRootClientId ) {
const sectionClientIds =
select.getBlockOrder( sectionRootClientId );
+ const lastSectionClientId =
+ sectionClientIds[ sectionClientIds.length - 1 ];
if ( sectionClientIds ) {
- const parents = select.getBlockParents(
- firstSelectedClientId
- );
- const firstSectionClientId = parents.find( ( parent ) =>
- sectionClientIds.includes( parent )
- );
- dispatch.selectBlock( firstSectionClientId );
+ if ( firstSelectedClientId ) {
+ const parents = select.getBlockParents(
+ firstSelectedClientId
+ );
+ const firstSectionClientId = parents.find( ( parent ) =>
+ sectionClientIds.includes( parent )
+ );
+ if ( firstSectionClientId ) {
+ dispatch.selectBlock( firstSectionClientId );
+ } else {
+ dispatch.selectBlock( lastSectionClientId );
+ }
+ } else {
+ dispatch.selectBlock( lastSectionClientId );
+ }
}
} else if ( firstSelectedClientId ) {
const rootClientId = select.getBlockHierarchyRootClientId(
firstSelectedClientId
);
dispatch.selectBlock( rootClientId );
+ } else {
+ // If there's no block selected and no sectionRootClientId, select the last root block.
+ const lastRootBlock = allBlocks[ allBlocks.length - 1 ];
+ dispatch.selectBlock( lastRootBlock?.clientId );
}
}
diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js
index 85d624e048318f..6cfccb17287ff9 100644
--- a/packages/block-editor/src/store/private-actions.js
+++ b/packages/block-editor/src/store/private-actions.js
@@ -7,6 +7,8 @@ import { Platform } from '@wordpress/element';
* Internal dependencies
*/
import { undoIgnoreBlocks } from './undo-ignore';
+import { store as blockEditorStore } from './index';
+import { unlock } from '../lock-unlock';
const castArray = ( maybeArray ) =>
Array.isArray( maybeArray ) ? maybeArray : [ maybeArray ];
@@ -339,9 +341,10 @@ export function setLastFocus( lastFocus = null ) {
* @param {string} clientId The block's clientId.
*/
export function stopEditingAsBlocks( clientId ) {
- return ( { select, dispatch } ) => {
- const focusModeToRevert =
- select.__unstableGetTemporarilyEditingFocusModeToRevert();
+ return ( { select, dispatch, registry } ) => {
+ const focusModeToRevert = unlock(
+ registry.select( blockEditorStore )
+ ).getTemporarilyEditingFocusModeToRevert();
dispatch.__unstableMarkNextChangeAsNotPersistent();
dispatch.updateBlockAttributes( clientId, {
templateLock: 'contentOnly',
diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js
index 126c116ac97f84..94de85cbabe70e 100644
--- a/packages/block-editor/src/store/private-selectors.js
+++ b/packages/block-editor/src/store/private-selectors.js
@@ -12,6 +12,8 @@ import {
getBlockEditingMode,
getSettings,
canInsertBlockType,
+ getBlockName,
+ getTemplateLock,
} from './selectors';
import {
checkAllowListRecursive,
@@ -420,3 +422,53 @@ export function isDragging( state ) {
export function getExpandedBlock( state ) {
return state.expandedBlock;
}
+
+/**
+ * Retrieves the client ID of the ancestor block that is content locking the block
+ * with the provided client ID.
+ *
+ * @param {Object} state Global application state.
+ * @param {Object} clientId Client Id of the block.
+ *
+ * @return {?string} Client ID of the ancestor block that is content locking the block.
+ */
+export const getContentLockingParent = createSelector(
+ ( state, clientId ) => {
+ let current = clientId;
+ let result;
+ while ( ( current = state.blocks.parents.get( current ) ) ) {
+ if (
+ getBlockName( state, current ) === 'core/block' ||
+ getTemplateLock( state, current ) === 'contentOnly'
+ ) {
+ result = current;
+ }
+ }
+ return result;
+ },
+ ( state ) => [ state.blocks.parents, state.blockListSettings ]
+);
+
+/**
+ * Retrieves the client ID of the block that is content locked but is
+ * currently being temporarily edited as a non-locked block.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {?string} The client ID of the block being temporarily edited as a non-locked block.
+ */
+export function getTemporarilyEditingAsBlocks( state ) {
+ return state.temporarilyEditingAsBlocks;
+}
+
+/**
+ * Returns the focus mode that should be reapplied when the user stops editing
+ * a content locked blocks as a block without locking.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {?string} The focus mode that should be re-set when temporarily editing as blocks stops.
+ */
+export function getTemporarilyEditingFocusModeToRevert( state ) {
+ return state.temporarilyEditingFocusModeRevert;
+}
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index 43b1990e3f17b4..19c609e848732c 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -31,6 +31,12 @@ import { orderBy } from '../utils/sorting';
import { STORE_NAME } from './constants';
import { unlock } from '../lock-unlock';
+import {
+ getContentLockingParent,
+ getTemporarilyEditingAsBlocks,
+ getTemporarilyEditingFocusModeToRevert,
+} from './private-selectors';
+
/**
* A block selection object.
*
@@ -2756,50 +2762,6 @@ export const __unstableGetVisibleBlocks = createSelector(
( state ) => [ state.blockVisibility ]
);
-/**
- * DO-NOT-USE in production.
- * This selector is created for internal/experimental only usage and may be
- * removed anytime without any warning, causing breakage on any plugin or theme invoking it.
- */
-export const __unstableGetContentLockingParent = createSelector(
- ( state, clientId ) => {
- let current = clientId;
- let result;
- while ( ( current = state.blocks.parents.get( current ) ) ) {
- if (
- getBlockName( state, current ) === 'core/block' ||
- getTemplateLock( state, current ) === 'contentOnly'
- ) {
- result = current;
- }
- }
- return result;
- },
- ( state ) => [ state.blocks.parents, state.blockListSettings ]
-);
-
-/**
- * DO-NOT-USE in production.
- * This selector is created for internal/experimental only usage and may be
- * removed anytime without any warning, causing breakage on any plugin or theme invoking it.
- *
- * @param {Object} state Global application state.
- */
-export function __unstableGetTemporarilyEditingAsBlocks( state ) {
- return state.temporarilyEditingAsBlocks;
-}
-
-/**
- * DO-NOT-USE in production.
- * This selector is created for internal/experimental only usage and may be
- * removed anytime without any warning, causing breakage on any plugin or theme invoking it.
- *
- * @param {Object} state Global application state.
- */
-export function __unstableGetTemporarilyEditingFocusModeToRevert( state ) {
- return state.temporarilyEditingFocusModeRevert;
-}
-
export function __unstableHasActiveBlockOverlayActive( state, clientId ) {
// Prevent overlay on blocks with a non-default editing mode. If the mdoe is
// 'disabled' then the overlay is redundant since the block can't be
@@ -3013,3 +2975,66 @@ export const isGroupable = createRegistrySelector(
);
}
);
+
+/**
+ * DO-NOT-USE in production.
+ * This selector is created for internal/experimental only usage and may be
+ * removed anytime without any warning, causing breakage on any plugin or theme invoking it.
+ *
+ * @deprecated
+ *
+ * @param {Object} state Global application state.
+ * @param {Object} clientId Client Id of the block.
+ *
+ * @return {?string} Client ID of the ancestor block that is content locking the block.
+ */
+export const __unstableGetContentLockingParent = ( state, clientId ) => {
+ deprecated(
+ "wp.data.select( 'core/block-editor' ).__unstableGetContentLockingParent",
+ {
+ since: '6.1',
+ version: '6.7',
+ }
+ );
+ return getContentLockingParent( state, clientId );
+};
+
+/**
+ * DO-NOT-USE in production.
+ * This selector is created for internal/experimental only usage and may be
+ * removed anytime without any warning, causing breakage on any plugin or theme invoking it.
+ *
+ * @deprecated
+ *
+ * @param {Object} state Global application state.
+ */
+export function __unstableGetTemporarilyEditingAsBlocks( state ) {
+ deprecated(
+ "wp.data.select( 'core/block-editor' ).__unstableGetTemporarilyEditingAsBlocks",
+ {
+ since: '6.1',
+ version: '6.7',
+ }
+ );
+ return getTemporarilyEditingAsBlocks( state );
+}
+
+/**
+ * DO-NOT-USE in production.
+ * This selector is created for internal/experimental only usage and may be
+ * removed anytime without any warning, causing breakage on any plugin or theme invoking it.
+ *
+ * @deprecated
+ *
+ * @param {Object} state Global application state.
+ */
+export function __unstableGetTemporarilyEditingFocusModeToRevert( state ) {
+ deprecated(
+ "wp.data.select( 'core/block-editor' ).__unstableGetTemporarilyEditingFocusModeToRevert",
+ {
+ since: '6.5',
+ version: '6.7',
+ }
+ );
+ return getTemporarilyEditingFocusModeToRevert( state );
+}
diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md
index c3153990e2f0ea..91741cf70413f9 100644
--- a/packages/block-library/CHANGELOG.md
+++ b/packages/block-library/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased
+## 8.33.0 (2024-04-19)
+
## 8.32.0 (2024-04-03)
## 8.31.0 (2024-03-21)
diff --git a/packages/block-library/package.json b/packages/block-library/package.json
index d9bb73400d79cd..c4cded1866a731 100644
--- a/packages/block-library/package.json
+++ b/packages/block-library/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/block-library",
- "version": "8.32.0",
+ "version": "8.33.0",
"description": "Block library for the WordPress editor.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -52,6 +52,7 @@
"@wordpress/icons": "file:../icons",
"@wordpress/interactivity": "file:../interactivity",
"@wordpress/interactivity-router": "file:../interactivity-router",
+ "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts",
"@wordpress/keycodes": "file:../keycodes",
"@wordpress/notices": "file:../notices",
"@wordpress/patterns": "file:../patterns",
diff --git a/packages/block-library/src/block-keyboard-shortcuts/index.js b/packages/block-library/src/block-keyboard-shortcuts/index.js
new file mode 100644
index 00000000000000..6d9cde8364001f
--- /dev/null
+++ b/packages/block-library/src/block-keyboard-shortcuts/index.js
@@ -0,0 +1,113 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect } from '@wordpress/element';
+import { useSelect, useDispatch } from '@wordpress/data';
+import {
+ useShortcut,
+ store as keyboardShortcutsStore,
+} from '@wordpress/keyboard-shortcuts';
+import { __ } from '@wordpress/i18n';
+import { createBlock } from '@wordpress/blocks';
+import { store as blockEditorStore } from '@wordpress/block-editor';
+
+function BlockKeyboardShortcuts() {
+ const { registerShortcut } = useDispatch( keyboardShortcutsStore );
+ const { replaceBlocks } = useDispatch( blockEditorStore );
+ const { getBlockName, getSelectedBlockClientId, getBlockAttributes } =
+ useSelect( blockEditorStore );
+
+ const handleTransformHeadingAndParagraph = ( event, level ) => {
+ event.preventDefault();
+
+ const currentClientId = getSelectedBlockClientId();
+ if ( currentClientId === null ) {
+ return;
+ }
+
+ const blockName = getBlockName( currentClientId );
+ const isParagraph = blockName === 'core/paragraph';
+ const isHeading = blockName === 'core/heading';
+
+ if ( ! isParagraph && ! isHeading ) {
+ return;
+ }
+
+ const destinationBlockName =
+ level === 0 ? 'core/paragraph' : 'core/heading';
+
+ const attributes = getBlockAttributes( currentClientId );
+
+ // Avoid unnecessary block transform when attempting to transform to
+ // the same block type and/or same level.
+ if (
+ ( isParagraph && level === 0 ) ||
+ ( isHeading && attributes.level === level )
+ ) {
+ return;
+ }
+
+ const textAlign =
+ blockName === 'core/paragraph' ? 'align' : 'textAlign';
+ const destinationTextAlign =
+ destinationBlockName === 'core/paragraph' ? 'align' : 'textAlign';
+
+ replaceBlocks(
+ currentClientId,
+ createBlock( destinationBlockName, {
+ level,
+ content: attributes.content,
+ ...{ [ destinationTextAlign ]: attributes[ textAlign ] },
+ } )
+ );
+ };
+
+ useEffect( () => {
+ registerShortcut( {
+ name: 'core/block-editor/transform-heading-to-paragraph',
+ category: 'block-library',
+ description: __( 'Transform heading to paragraph.' ),
+ keyCombination: {
+ modifier: 'access',
+ character: '0',
+ },
+ aliases: [
+ {
+ modifier: 'access',
+ character: '7',
+ },
+ ],
+ } );
+
+ [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => {
+ registerShortcut( {
+ name: `core/block-editor/transform-paragraph-to-heading-${ level }`,
+ category: 'block-library',
+ description: __( 'Transform paragraph to heading.' ),
+ keyCombination: {
+ modifier: 'access',
+ character: `${ level }`,
+ },
+ } );
+ } );
+ }, [] );
+
+ useShortcut(
+ 'core/block-editor/transform-heading-to-paragraph',
+ ( event ) => handleTransformHeadingAndParagraph( event, 0 )
+ );
+
+ [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => {
+ //the loop is based off on a constant therefore
+ //the hook will execute the same way every time
+ //eslint-disable-next-line react-hooks/rules-of-hooks
+ useShortcut(
+ `core/block-editor/transform-paragraph-to-heading-${ level }`,
+ ( event ) => handleTransformHeadingAndParagraph( event, level )
+ );
+ } );
+
+ return null;
+}
+
+export default BlockKeyboardShortcuts;
diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss
index 76cff33bbf00cd..ea6fa8836a3a4b 100644
--- a/packages/block-library/src/gallery/editor.scss
+++ b/packages/block-library/src/gallery/editor.scss
@@ -1,4 +1,4 @@
-figure.wp-block-gallery {
+:where(figure.wp-block-gallery) {
// Override the default list style type _only in the editor_
// to avoid :not() selector specificity issues.
// See https://github.com/WordPress/gutenberg/pull/10358
diff --git a/packages/block-library/src/group/index.js b/packages/block-library/src/group/index.js
index 2d06f1a965c521..1ccfa4fa89c1aa 100644
--- a/packages/block-library/src/group/index.js
+++ b/packages/block-library/src/group/index.js
@@ -22,14 +22,6 @@ export { metadata, name };
export const settings = {
icon,
example: {
- attributes: {
- style: {
- color: {
- text: '#000000',
- background: '#ffffff',
- },
- },
- },
innerBlocks: [
{
name: 'core/paragraph',
diff --git a/packages/block-library/src/group/style.scss b/packages/block-library/src/group/style.scss
index 3457a12adb6221..018091ffd45644 100644
--- a/packages/block-library/src/group/style.scss
+++ b/packages/block-library/src/group/style.scss
@@ -2,3 +2,8 @@
// This block has customizable padding, border-box makes that more predictable.
box-sizing: border-box;
}
+
+// We need this so groups with negative margins overlap as expected.
+:where(.wp-block-group.wp-block-group-is-layout-constrained) {
+ position: relative;
+}
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index e2e0fd9e414ef3..9cb2f44d05eb9b 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -330,3 +330,5 @@ export const __experimentalRegisterExperimentalCoreBlocks = process.env
.forEach( ( { init } ) => init() );
}
: undefined;
+
+export { privateApis } from './private-apis';
diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js
index 0efe538b01f629..1ef7bd5e5fed7b 100644
--- a/packages/block-library/src/latest-posts/edit.js
+++ b/packages/block-library/src/latest-posts/edit.js
@@ -431,7 +431,7 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) {
const dateFormat = getSettings().formats.date;
return (
-
+ <>
{ inspectorControls }
@@ -518,7 +518,6 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) {
{ addLinkToFeaturedImage ? (
) }
-
+ >
);
}
diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php
index 6bff971beef576..913a9ae2d430e1 100644
--- a/packages/block-library/src/latest-posts/index.php
+++ b/packages/block-library/src/latest-posts/index.php
@@ -157,7 +157,7 @@ function render_block_core_latest_posts( $attributes ) {
$trimmed_excerpt = substr( $trimmed_excerpt, 0, -11 );
$trimmed_excerpt .= sprintf(
/* translators: 1: A URL to a post, 2: Hidden accessibility text: Post title */
- __( 'β¦ Read more: %2$s' ),
+ __( 'β¦ Read more: %2$s' ),
esc_url( $post_link ),
esc_html( $title )
);
diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js
index d71a49aef969c0..e8370242b7870c 100644
--- a/packages/block-library/src/navigation-link/edit.js
+++ b/packages/block-library/src/navigation-link/edit.js
@@ -153,6 +153,82 @@ function getMissingText( type ) {
return missingText;
}
+/*
+ * Warning, this duplicated in
+ * packages/block-library/src/navigation-submenu/edit.js
+ * Consider reuseing this components for both blocks.
+ */
+function Controls( { attributes, setAttributes, setIsLabelFieldFocused } ) {
+ const { label, url, description, title, rel } = attributes;
+ return (
+
+ {
+ setAttributes( { label: labelValue } );
+ } }
+ label={ __( 'Text' ) }
+ autoComplete="off"
+ onFocus={ () => setIsLabelFieldFocused( true ) }
+ onBlur={ () => setIsLabelFieldFocused( false ) }
+ />
+ {
+ updateAttributes(
+ { url: urlValue },
+ setAttributes,
+ attributes
+ );
+ } }
+ label={ __( 'Link' ) }
+ autoComplete="off"
+ />
+ {
+ setAttributes( { description: descriptionValue } );
+ } }
+ label={ __( 'Description' ) }
+ help={ __(
+ 'The description will be displayed in the menu if the current theme supports it.'
+ ) }
+ />
+ {
+ setAttributes( { title: titleValue } );
+ } }
+ label={ __( 'Title attribute' ) }
+ autoComplete="off"
+ help={ __(
+ 'Additional information to help clarify the purpose of the link.'
+ ) }
+ />
+ {
+ setAttributes( { rel: relValue } );
+ } }
+ label={ __( 'Rel attribute' ) }
+ autoComplete="off"
+ help={ __(
+ 'The relationship of the linked URL as space-separated link types.'
+ ) }
+ />
+
+ );
+}
+
export default function NavigationLinkEdit( {
attributes,
isSelected,
@@ -163,7 +239,7 @@ export default function NavigationLinkEdit( {
context,
clientId,
} ) {
- const { id, label, type, url, description, rel, title, kind } = attributes;
+ const { id, label, type, url, description, kind } = attributes;
const [ isInvalid, isDraft ] = useIsInvalidLink( kind, type, id );
const { maxNestingLevel } = context;
@@ -420,71 +496,11 @@ export default function NavigationLinkEdit( {
{ /* Warning, this duplicated in packages/block-library/src/navigation-submenu/edit.js */ }
-
- {
- setAttributes( { label: labelValue } );
- } }
- label={ __( 'Text' ) }
- autoComplete="off"
- onFocus={ () => setIsLabelFieldFocused( true ) }
- onBlur={ () => setIsLabelFieldFocused( false ) }
- />
- {
- updateAttributes(
- { url: urlValue },
- setAttributes,
- attributes
- );
- } }
- label={ __( 'Link' ) }
- autoComplete="off"
- />
- {
- setAttributes( { description: descriptionValue } );
- } }
- label={ __( 'Description' ) }
- help={ __(
- 'The description will be displayed in the menu if the current theme supports it.'
- ) }
- />
- {
- setAttributes( { title: titleValue } );
- } }
- label={ __( 'Title attribute' ) }
- autoComplete="off"
- help={ __(
- 'Additional information to help clarify the purpose of the link.'
- ) }
- />
- {
- setAttributes( { rel: relValue } );
- } }
- label={ __( 'Rel attribute' ) }
- autoComplete="off"
- help={ __(
- 'The relationship of the linked URL as space-separated link types.'
- ) }
- />
-
+
{ /* eslint-disable jsx-a11y/anchor-is-valid */ }
diff --git a/packages/block-library/src/navigation-link/index.php b/packages/block-library/src/navigation-link/index.php
index ffeea51996a02c..5653e04fca88a3 100644
--- a/packages/block-library/src/navigation-link/index.php
+++ b/packages/block-library/src/navigation-link/index.php
@@ -198,6 +198,13 @@ function render_block_core_navigation_link( $attributes, $content, $block ) {
$kind = empty( $attributes['kind'] ) ? 'post_type' : str_replace( '-', '_', $attributes['kind'] );
$is_active = ! empty( $attributes['id'] ) && get_queried_object_id() === (int) $attributes['id'] && ! empty( get_queried_object()->$kind );
+ if ( is_post_type_archive() ) {
+ $queried_archive_link = get_post_type_archive_link( get_queried_object()->name );
+ if ( $attributes['url'] === $queried_archive_link ) {
+ $is_active = true;
+ }
+ }
+
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $css_classes . ' wp-block-navigation-item' . ( $has_submenu ? ' has-child' : '' ) .
diff --git a/packages/block-library/src/navigation-submenu/index.php b/packages/block-library/src/navigation-submenu/index.php
index b67f5ead8651ee..92b55e291606e8 100644
--- a/packages/block-library/src/navigation-submenu/index.php
+++ b/packages/block-library/src/navigation-submenu/index.php
@@ -87,6 +87,13 @@ function render_block_core_navigation_submenu( $attributes, $content, $block ) {
$kind = empty( $attributes['kind'] ) ? 'post_type' : str_replace( '-', '_', $attributes['kind'] );
$is_active = ! empty( $attributes['id'] ) && get_queried_object_id() === (int) $attributes['id'] && ! empty( get_queried_object()->$kind );
+ if ( is_post_type_archive() ) {
+ $queried_archive_link = get_post_type_archive_link( get_queried_object()->name );
+ if ( $attributes['url'] === $queried_archive_link ) {
+ $is_active = true;
+ }
+ }
+
$show_submenu_indicators = isset( $block->context['showSubmenuIcon'] ) && $block->context['showSubmenuIcon'];
$open_on_click = isset( $block->context['openSubmenusOnClick'] ) && $block->context['openSubmenusOnClick'];
$open_on_hover_and_click = isset( $block->context['openSubmenusOnClick'] ) && ! $block->context['openSubmenusOnClick'] &&
diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json
index eef6af390de78a..c65e0c6224616e 100644
--- a/packages/block-library/src/navigation/block.json
+++ b/packages/block-library/src/navigation/block.json
@@ -137,15 +137,6 @@
"type": "flex"
}
},
- "__experimentalStyle": {
- "elements": {
- "link": {
- "color": {
- "text": "inherit"
- }
- }
- }
- },
"interactivity": true,
"renaming": false
},
diff --git a/packages/block-library/src/navigation/edit/deleted-navigation-warning.js b/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
index 143bf8a3a2ce33..915e19694085de 100644
--- a/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
+++ b/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
@@ -11,7 +11,7 @@ function DeletedNavigationWarning( { onCreateNew } ) {
{ createInterpolateElement(
__(
- 'Navigation Menu has been deleted or is unavailable. '
+ 'Navigation Menu has been deleted or is unavailable. '
),
{
button: ,
diff --git a/packages/block-library/src/navigation/edit/navigation-menu-selector.js b/packages/block-library/src/navigation/edit/navigation-menu-selector.js
index 47d81172c1853c..007220b1789131 100644
--- a/packages/block-library/src/navigation/edit/navigation-menu-selector.js
+++ b/packages/block-library/src/navigation/edit/navigation-menu-selector.js
@@ -202,7 +202,7 @@ function NavigationMenuSelector( {
! hasResolvedNavigationMenus
}
>
- { __( 'Create new menu' ) }
+ { __( 'Create new Menu' ) }
) }
diff --git a/packages/block-library/src/navigation/edit/test/navigation-menu-selector.js b/packages/block-library/src/navigation/edit/test/navigation-menu-selector.js
index 3d2066efaaa141..eee9be647cb19c 100644
--- a/packages/block-library/src/navigation/edit/test/navigation-menu-selector.js
+++ b/packages/block-library/src/navigation/edit/test/navigation-menu-selector.js
@@ -191,7 +191,7 @@ describe( 'NavigationMenuSelector', () => {
expect( toolsGroup ).toBeInTheDocument();
const createMenuButton = screen.getByRole( 'menuitem', {
- name: 'Create new menu',
+ name: 'Create new Menu',
} );
expect( createMenuButton ).toBeInTheDocument();
@@ -236,7 +236,7 @@ describe( 'NavigationMenuSelector', () => {
await user.click( toggleButton );
const createMenuButton = screen.getByRole( 'menuitem', {
- name: 'Create new menu',
+ name: 'Create new Menu',
} );
await user.click( createMenuButton );
@@ -268,7 +268,7 @@ describe( 'NavigationMenuSelector', () => {
await user.click(
screen.getByRole( 'menuitem', {
- name: 'Create new menu',
+ name: 'Create new Menu',
} )
);
@@ -293,7 +293,7 @@ describe( 'NavigationMenuSelector', () => {
// Check the "Create menu" button is disabled.
expect(
screen.queryByRole( 'menuitem', {
- name: 'Create new menu',
+ name: 'Create new Menu',
} )
).toBeDisabled();
@@ -318,7 +318,7 @@ describe( 'NavigationMenuSelector', () => {
// Check the button is enabled again.
expect(
screen.queryByRole( 'menuitem', {
- name: 'Create new menu',
+ name: 'Create new Menu',
} )
).toBeEnabled();
} );
diff --git a/packages/block-library/src/page-list/convert-to-links-modal.js b/packages/block-library/src/page-list/convert-to-links-modal.js
index f47b5e3de259dd..8f8c75f6dea1c1 100644
--- a/packages/block-library/src/page-list/convert-to-links-modal.js
+++ b/packages/block-library/src/page-list/convert-to-links-modal.js
@@ -5,7 +5,7 @@ import { Button, Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
export const convertDescription = __(
- "This navigation menu displays your website's pages. Editing it will enable you to add, delete, or reorder pages. However, new pages will no longer be added automatically."
+ "This Navigation Menu displays your website's pages. Editing it will enable you to add, delete, or reorder pages. However, new pages will no longer be added automatically."
);
export function ConvertToLinksModal( { onClick, onClose, disabled } ) {
diff --git a/packages/block-library/src/private-apis.js b/packages/block-library/src/private-apis.js
new file mode 100644
index 00000000000000..3cee106895d6d7
--- /dev/null
+++ b/packages/block-library/src/private-apis.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import { default as BlockKeyboardShortcuts } from './block-keyboard-shortcuts';
+import { lock } from './lock-unlock';
+
+/**
+ * @private
+ */
+export const privateApis = {};
+lock( privateApis, {
+ BlockKeyboardShortcuts,
+} );
diff --git a/packages/block-library/src/query/edit/enhanced-pagination-modal.js b/packages/block-library/src/query/edit/enhanced-pagination-modal.js
index 21838184f6bd6a..bc8e05f67a01ee 100644
--- a/packages/block-library/src/query/edit/enhanced-pagination-modal.js
+++ b/packages/block-library/src/query/edit/enhanced-pagination-modal.js
@@ -27,7 +27,11 @@ export default function EnhancedPaginationModal( {
useUnsupportedBlocks( clientId );
useEffect( () => {
- if ( enhancedPagination && hasUnsupportedBlocks ) {
+ if (
+ enhancedPagination &&
+ hasUnsupportedBlocks &&
+ ! window.__experimentalFullPageClientSideNavigation
+ ) {
setAttributes( { enhancedPagination: false } );
setOpen( true );
}
diff --git a/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js b/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js
index de889c0715c07a..293baead3f5c62 100644
--- a/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js
+++ b/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js
@@ -15,9 +15,15 @@ export default function EnhancedPaginationControl( {
clientId,
} ) {
const { hasUnsupportedBlocks } = useUnsupportedBlocks( clientId );
+ const fullPageClientSideNavigation =
+ window.__experimentalFullPageClientSideNavigation;
let help = __( 'Browsing between pages requires a full page reload.' );
- if ( enhancedPagination ) {
+ if ( fullPageClientSideNavigation ) {
+ help = __(
+ 'Experimental full-page client-side navigation setting enabled.'
+ );
+ } else if ( enhancedPagination ) {
help = __(
"Browsing between pages won't require a full page reload, unless non-compatible blocks are detected."
);
@@ -32,8 +38,12 @@ export default function EnhancedPaginationControl( {
{
setAttributes( {
enhancedPagination: ! value,
diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php
index c9f9d1ab9431fb..6cc57dc08388c6 100644
--- a/packages/block-library/src/query/index.php
+++ b/packages/block-library/src/query/index.php
@@ -51,8 +51,8 @@ function render_block_core_query( $attributes, $content, $block ) {
// Add the necessary directives.
$p->set_attribute( 'data-wp-interactive', 'core/query' );
$p->set_attribute( 'data-wp-router-region', 'query-' . $attributes['queryId'] );
- $p->set_attribute( 'data-wp-init', 'callbacks.setQueryRef' );
$p->set_attribute( 'data-wp-context', '{}' );
+ $p->set_attribute( 'data-wp-key', $attributes['queryId'] );
$content = $p->get_updated_html();
}
}
diff --git a/packages/block-library/src/social-link/block.json b/packages/block-library/src/social-link/block.json
index d487465ef79664..37e8376f22ff09 100644
--- a/packages/block-library/src/social-link/block.json
+++ b/packages/block-library/src/social-link/block.json
@@ -5,7 +5,7 @@
"title": "Social Icon",
"category": "widgets",
"parent": [ "core/social-links" ],
- "description": "Display an icon linking to a social media profile or site.",
+ "description": "Display an icon linking to a social profile or site.",
"textdomain": "default",
"attributes": {
"url": {
diff --git a/packages/block-library/src/social-link/edit.js b/packages/block-library/src/social-link/edit.js
index e08f8e6697cbae..8873a4ae220821 100644
--- a/packages/block-library/src/social-link/edit.js
+++ b/packages/block-library/src/social-link/edit.js
@@ -23,7 +23,7 @@ import {
PanelRow,
TextControl,
} from '@wordpress/components';
-import { __, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import { keyboardReturn } from '@wordpress/icons';
/**
@@ -58,7 +58,9 @@ const SocialLinkURLPopover = ( {
onChange={ ( nextURL ) =>
setAttributes( { url: nextURL } )
}
- placeholder={ __( 'Enter address' ) }
+ placeholder={ __( 'Enter social link' ) }
+ label={ __( 'Enter social link' ) }
+ hideLabelFromVision
disableSuggestions
onKeyDown={ ( event ) => {
if (
@@ -91,7 +93,7 @@ const SocialLinkEdit = ( {
setAttributes,
clientId,
} ) => {
- const { url, service, label, rel } = attributes;
+ const { url, service, label = '', rel } = attributes;
const {
showLabels,
iconColor,
@@ -113,7 +115,12 @@ const SocialLinkEdit = ( {
const IconComponent = getIconBySite( service );
const socialLinkName = getNameBySite( service );
- const socialLinkLabel = label ?? socialLinkName;
+ // The initial label (ie. the link text) is an empty string.
+ // We want to prevent empty links so that the link text always fallbacks to
+ // the social name, even when users enter and save an empty string or only
+ // spaces. The PHP render callback fallbacks to the social name as well.
+ const socialLinkText = label.trim() === '' ? socialLinkName : label;
+
const blockProps = useBlockProps( {
className: classes,
style: {
@@ -125,25 +132,19 @@ const SocialLinkEdit = ( {
return (
<>
-
+
- setAttributes( { label: value || undefined } )
+ setAttributes( { label: value } )
}
+ placeholder={ socialLinkName }
/>
@@ -168,18 +169,18 @@ const SocialLinkEdit = ( {
'screen-reader-text': ! showLabels,
} ) }
>
- { socialLinkLabel }
+ { socialLinkText }
- { isSelected && showURLPopover && (
-
- ) }
+ { isSelected && showURLPopover && (
+
+ ) }
>
);
diff --git a/packages/block-library/src/social-link/index.php b/packages/block-library/src/social-link/index.php
index 69a145c0ff6028..5e2bc616d0e5ac 100644
--- a/packages/block-library/src/social-link/index.php
+++ b/packages/block-library/src/social-link/index.php
@@ -19,9 +19,11 @@
function render_block_core_social_link( $attributes, $content, $block ) {
$open_in_new_tab = isset( $block->context['openInNewTab'] ) ? $block->context['openInNewTab'] : false;
+ $text = ! empty( $attributes['label'] ) ? trim( $attributes['label'] ) : '';
+
$service = isset( $attributes['service'] ) ? $attributes['service'] : 'Icon';
$url = isset( $attributes['url'] ) ? $attributes['url'] : false;
- $label = ! empty( $attributes['label'] ) ? $attributes['label'] : block_core_social_link_get_name( $service );
+ $text = $text ? $text : block_core_social_link_get_name( $service );
$rel = isset( $attributes['rel'] ) ? $attributes['rel'] : '';
$show_labels = array_key_exists( 'showLabels', $block->context ) ? $block->context['showLabels'] : false;
@@ -57,7 +59,7 @@ function render_block_core_social_link( $attributes, $content, $block ) {
$link = '';
$link .= '';
$link .= $icon;
- $link .= '' . esc_html( $label ) . '';
+ $link .= '' . esc_html( $text ) . '';
$link .= '';
$processor = new WP_HTML_Tag_Processor( $link );
diff --git a/packages/block-library/src/social-links/block.json b/packages/block-library/src/social-links/block.json
index 0c8c7be1eba9a5..45dbd9d698953f 100644
--- a/packages/block-library/src/social-links/block.json
+++ b/packages/block-library/src/social-links/block.json
@@ -5,7 +5,7 @@
"title": "Social Icons",
"category": "widgets",
"allowedBlocks": [ "core/social-link" ],
- "description": "Display icons linking to your social media profiles or sites.",
+ "description": "Display icons linking to your social profiles or sites.",
"keywords": [ "links" ],
"textdomain": "default",
"attributes": {
diff --git a/packages/block-library/src/social-links/edit.js b/packages/block-library/src/social-links/edit.js
index 38a68e7f2dab2c..52d26be50a6c9e 100644
--- a/packages/block-library/src/social-links/edit.js
+++ b/packages/block-library/src/social-links/edit.js
@@ -203,7 +203,7 @@ export function SocialLinksEdit( props ) {
/>
setAttributes( { showLabels: ! showLabels } )
diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js
index 5af3ede02b677e..52d740735eaf61 100644
--- a/packages/block-library/src/template-part/edit/index.js
+++ b/packages/block-library/src/template-part/edit/index.js
@@ -128,6 +128,7 @@ export default function TemplatePartEdit( {
area,
onNavigateToEntityRecord,
title,
+ canEditTemplate,
} = useSelect(
( select ) => {
const { getEditedEntityRecord, hasFinishedResolution } =
@@ -150,6 +151,9 @@ export default function TemplatePartEdit( {
)
: false;
+ const _canEditTemplate =
+ select( coreStore ).canUser( 'create', 'templates' ) ?? false;
+
return {
hasInnerBlocks: getBlockCount( clientId ) > 0,
isResolved: hasResolvedEntity,
@@ -161,6 +165,7 @@ export default function TemplatePartEdit( {
onNavigateToEntityRecord:
getSettings().onNavigateToEntityRecord,
title: entityRecord?.title,
+ canEditTemplate: _canEditTemplate,
};
},
[ templatePartId, attributes.area, clientId ]
@@ -228,20 +233,22 @@ export default function TemplatePartEdit( {
return (
<>
- { isEntityAvailable && onNavigateToEntityRecord && (
-
-
- onNavigateToEntityRecord( {
- postId: templatePartId,
- postType: 'wp_template_part',
- } )
- }
- >
- { __( 'Edit' ) }
-
-
- ) }
+ { isEntityAvailable &&
+ onNavigateToEntityRecord &&
+ canEditTemplate && (
+
+
+ onNavigateToEntityRecord( {
+ postId: templatePartId,
+ postType: 'wp_template_part',
+ } )
+ }
+ >
+ { __( 'Edit' ) }
+
+
+ ) }
{
* ```
*/
export const registerBlockVariation = ( blockName, variation ) => {
+ if ( typeof variation.name !== 'string' ) {
+ console.warn( 'Variation names must be unique strings.' );
+ }
+
dispatch( blocksStore ).addBlockVariations( blockName, variation );
};
diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js
index 8433bbcb04ca38..81f45f1999803e 100644
--- a/packages/blocks/src/api/test/registration.js
+++ b/packages/blocks/src/api/test/registration.js
@@ -13,6 +13,7 @@ import { select, dispatch } from '@wordpress/data';
import {
registerBlockType,
registerBlockCollection,
+ registerBlockVariation,
unregisterBlockCollection,
unregisterBlockType,
setFreeformContentHandlerName,
@@ -26,6 +27,7 @@ import {
getBlockType,
getBlockTypes,
getBlockSupport,
+ getBlockVariations,
hasBlockSupport,
isReusableBlock,
unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase
@@ -398,39 +400,47 @@ describe( 'blocks', () => {
} );
} );
- // This can be removed once polyfill adding selectors has been removed.
- it( 'should apply selectors on the client when not set on the server', () => {
- const blockName = 'core/test-block-with-selectors';
+ it( 'should merge settings provided by server and client', () => {
+ const blockName = 'core/test-block-with-merged-settings';
unstable__bootstrapServerSideBlockDefinitions( {
[ blockName ]: {
- category: 'widgets',
- },
- } );
- unstable__bootstrapServerSideBlockDefinitions( {
- [ blockName ]: {
- selectors: { root: '.wp-block-custom-selector' },
- category: 'ignored',
+ variations: [
+ { name: 'foo', label: 'Foo' },
+ { name: 'baz', label: 'Baz', description: 'Testing' },
+ ],
},
} );
const blockType = {
- title: 'block title',
+ title: 'block settings merge',
+ variations: [
+ { name: 'bar', label: 'Bar' },
+ { name: 'baz', label: 'Baz', icon: 'layout' },
+ ],
};
registerBlockType( blockName, blockType );
expect( getBlockType( blockName ) ).toEqual( {
name: blockName,
save: expect.any( Function ),
- title: 'block title',
- category: 'widgets',
+ title: 'block settings merge',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
- selectors: { root: '.wp-block-custom-selector' },
+ selectors: {},
supports: {},
styles: [],
- variations: [],
+ variations: [
+ { name: 'foo', label: 'Foo' },
+ {
+ description: 'Testing',
+ name: 'baz',
+ label: 'Baz',
+ icon: 'layout',
+ },
+ { name: 'bar', label: 'Bar' },
+ ],
blockHooks: {},
} );
} );
@@ -1402,6 +1412,26 @@ describe( 'blocks', () => {
expect( isReusableBlock( block ) ).toBe( false );
} );
} );
+
+ describe( 'registerBlockVariation', () => {
+ it( 'should warn when registering block variation without a name', () => {
+ registerBlockType( 'core/variation-block', defaultBlockSettings );
+ registerBlockVariation( 'core/variation-block', {
+ title: 'Variation Title',
+ description: 'Variation description',
+ } );
+
+ expect( console ).toHaveWarnedWith(
+ 'Variation names must be unique strings.'
+ );
+ expect( getBlockVariations( 'core/variation-block' ) ).toEqual( [
+ {
+ title: 'Variation Title',
+ description: 'Variation description',
+ },
+ ] );
+ } );
+ } );
} );
/* eslint-enable react/forbid-elements */
diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js
index d609f70b91b55d..1ef9c3614922e0 100644
--- a/packages/blocks/src/store/private-actions.js
+++ b/packages/blocks/src/store/private-actions.js
@@ -51,7 +51,9 @@ export function registerBlockBindingsSource( source ) {
type: 'REGISTER_BLOCK_BINDINGS_SOURCE',
sourceName: source.name,
sourceLabel: source.label,
- useSource: source.useSource,
+ getValue: source.getValue,
+ setValue: source.setValue,
+ getPlaceholder: source.getPlaceholder,
lockAttributesEditing: source.lockAttributesEditing,
};
}
diff --git a/packages/blocks/src/store/process-block-type.js b/packages/blocks/src/store/process-block-type.js
index 889f59b55e392e..154f74c6ab7296 100644
--- a/packages/blocks/src/store/process-block-type.js
+++ b/packages/blocks/src/store/process-block-type.js
@@ -33,6 +33,37 @@ const LEGACY_CATEGORY_MAPPING = {
layout: 'design',
};
+/**
+ * Merge block variations bootstrapped from the server and client.
+ *
+ * When a variation is registered in both places, its properties are merged.
+ *
+ * @param {Array} bootstrappedVariations - A block type variations from the server.
+ * @param {Array} clientVariations - A block type variations from the client.
+ * @return {Array} The merged array of block variations.
+ */
+function mergeBlockVariations(
+ bootstrappedVariations = [],
+ clientVariations = []
+) {
+ const result = [ ...bootstrappedVariations ];
+
+ clientVariations.forEach( ( clientVariation ) => {
+ const index = result.findIndex(
+ ( bootstrappedVariation ) =>
+ bootstrappedVariation.name === clientVariation.name
+ );
+
+ if ( index !== -1 ) {
+ result[ index ] = { ...result[ index ], ...clientVariation };
+ } else {
+ result.push( clientVariation );
+ }
+ } );
+
+ return result;
+}
+
/**
* Takes the unprocessed block type settings, merges them with block type metadata
* and applies all the existing filters for the registered block type.
@@ -46,6 +77,8 @@ const LEGACY_CATEGORY_MAPPING = {
export const processBlockType =
( name, blockSettings ) =>
( { select } ) => {
+ const bootstrappedBlockType = select.getBootstrappedBlockType( name );
+
const blockType = {
name,
icon: BLOCK_ICON_DEFAULT,
@@ -56,11 +89,14 @@ export const processBlockType =
selectors: {},
supports: {},
styles: [],
- variations: [],
blockHooks: {},
save: () => null,
- ...select.getBootstrappedBlockType( name ),
+ ...bootstrappedBlockType,
...blockSettings,
+ variations: mergeBlockVariations(
+ bootstrappedBlockType?.variations,
+ blockSettings?.variations
+ ),
};
const settings = applyFilters(
diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js
index f92fb376b530a7..7a3a866485e4a9 100644
--- a/packages/blocks/src/store/reducer.js
+++ b/packages/blocks/src/store/reducer.js
@@ -66,19 +66,6 @@ function bootstrappedBlockTypes( state = {}, action ) {
// Don't overwrite if already set. It covers the case when metadata
// was initialized from the server.
if ( serverDefinition ) {
- // The `selectors` prop is not yet included in the server provided
- // definitions and needs to be polyfilled. This can be removed when the
- // minimum supported WordPress is >= 6.3.
- if (
- serverDefinition.selectors === undefined &&
- blockType.selectors
- ) {
- newDefinition = {
- ...serverDefinition,
- selectors: blockType.selectors,
- };
- }
-
// The `blockHooks` prop is not yet included in the server provided
// definitions and needs to be polyfilled. This can be removed when the
// minimum supported WordPress is >= 6.4.
@@ -389,7 +376,9 @@ export function blockBindingsSources( state = {}, action ) {
...state,
[ action.sourceName ]: {
label: action.sourceLabel,
- useSource: action.useSource,
+ getValue: action.getValue,
+ setValue: action.setValue,
+ getPlaceholder: action.getPlaceholder,
lockAttributesEditing: action.lockAttributesEditing ?? true,
},
};
diff --git a/packages/browserslist-config/CHANGELOG.md b/packages/browserslist-config/CHANGELOG.md
index 6aa4230ab920a3..437c17c02caa32 100644
--- a/packages/browserslist-config/CHANGELOG.md
+++ b/packages/browserslist-config/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased
+## 5.39.0 (2024-04-19)
+
## 5.38.0 (2024-04-03)
## 5.37.0 (2024-03-21)
diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json
index 979d09608d5c7b..e04b70fb534bb9 100644
--- a/packages/browserslist-config/package.json
+++ b/packages/browserslist-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/browserslist-config",
- "version": "5.38.0",
+ "version": "5.39.0",
"description": "WordPress Browserslist shared configuration.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md
index 97fb8999300b8b..aee350fa5e99c4 100644
--- a/packages/commands/CHANGELOG.md
+++ b/packages/commands/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased
+## 0.27.0 (2024-04-19)
+
## 0.26.0 (2024-04-03)
## 0.25.0 (2024-03-21)
diff --git a/packages/commands/package.json b/packages/commands/package.json
index c3f7d3ead0f460..2dbcc5cf43d7e9 100644
--- a/packages/commands/package.json
+++ b/packages/commands/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/commands",
- "version": "0.26.0",
+ "version": "0.27.0",
"description": "Handles the commands menu.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index c6a32b22cdd6c9..612f2d4546655d 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -2,22 +2,47 @@
## Unreleased
+### Enhancements
+
+- `InputControl`: Add a password visibility toggle story ([#60898](https://github.com/WordPress/gutenberg/pull/60898)).
+- `View`: Fix prop types ([#60919](https://github.com/WordPress/gutenberg/pull/60919)).
+
+### Bug Fix
+
+- `SlotFill`: fixed missing `getServerSnapshot` parameter in slot map ([#60943](https://github.com/WordPress/gutenberg/pull/60943)).
+
+### Enhancements
+
+- `DropZone`: Avoid a media query on mount [#60546](https://github.com/WordPress/gutenberg/pull/60546)).
+- `ComboboxControl`: Simplify string normalization ([#60893](https://github.com/WordPress/gutenberg/pull/60893)).
+
+### Internal
+
+- `FontSizerPicker`: Improve docs for default units ([#60996](https://github.com/WordPress/gutenberg/pull/60996)).
+
+## 27.4.0 (2024-04-19)
+
### Deprecation
- `Navigation`: Soft deprecate component ([#59182](https://github.com/WordPress/gutenberg/pull/59182)).
### Enhancements
+- `Tooltip`: Make tests faster ([#60897](https://github.com/WordPress/gutenberg/pull/60897)).
- `ExternalLink`: Use unicode arrow instead of svg icon ([#60255](https://github.com/WordPress/gutenberg/pull/60255)).
- `ProgressBar`: Move the indicator width styles from emotion to a CSS variable ([#60388](https://github.com/WordPress/gutenberg/pull/60388)).
-- `Text`: Add `text-wrap: pretty;` to improve wrapping. ([#60164](https://github.com/WordPress/gutenberg/pull/60164)).
-- `Navigator`: Navigation to the active path doesn't create a new location history. ([#60561](https://github.com/WordPress/gutenberg/pull/60561))
-- `FormToggle`: Forwards ref to input. ([#60234](https://github.com/WordPress/gutenberg/pull/60234)).
-- `ToggleControl`: Forwards ref to FormToggle. ([#60234](https://github.com/WordPress/gutenberg/pull/60234)).
+- `Text`: Add `text-wrap: pretty;` to improve wrapping ([#60164](https://github.com/WordPress/gutenberg/pull/60164)).
+- `Navigator`: Navigation to the active path doesn't create a new location history ([#60561](https://github.com/WordPress/gutenberg/pull/60561)).
+- `FormToggle`: Forwards ref to input ([#60234](https://github.com/WordPress/gutenberg/pull/60234)).
+- `ToggleControl`: Forwards ref to FormToggle ([#60234](https://github.com/WordPress/gutenberg/pull/60234)).
+- `CheckboxControl`: Update help text alignment ([#60787](https://github.com/WordPress/gutenberg/pull/60787)).
### Bug Fix
+- `Truncate`: Fix link control link preview when it displays long URLs ([#60890](https://github.com/WordPress/gutenberg/pull/60890)).
+
- `ProgressBar`: Fix CSS variable with invalid value ([#60576](https://github.com/WordPress/gutenberg/pull/60576)).
+- `CheckboxControl`: Fix label text wrap ([#60787](https://github.com/WordPress/gutenberg/pull/60787)).
### Experimental
@@ -25,7 +50,12 @@
### Internal
+- Remove CSS hack for Internet Explorer 11 ([#60727](https://github.com/WordPress/gutenberg/pull/60727)).
- `CheckboxControl`: Streamline size styles ([#60475](https://github.com/WordPress/gutenberg/pull/60475)).
+- Deprecate `reduceMotion` util ([#60839](https://github.com/WordPress/gutenberg/pull/60839)).
+- `InputBase`: Simplify management of focus styles. Affects all components based on `InputControl` (e.g. `SearchControl`, `NumberControl`, `UnitControl`), as well as `SelectControl`, `CustomSelectControl`, and `TreeSelect` ([#60226](https://github.com/WordPress/gutenberg/pull/60226)).
+- Removed dependency on `valtio`, replaced its usage in `SlotFill` with a custom object [#60879](https://github.com/WordPress/gutenberg/pull/60879)).
+- `CustomSelectControlV2`: Support disabled in item types ([#60896](https://github.com/WordPress/gutenberg/pull/60896)).
## 27.3.0 (2024-04-03)
@@ -60,6 +90,7 @@
- `TextControl`: Add typings for `date`, `time` and `datetime-local` ([#59666](https://github.com/WordPress/gutenberg/pull/59666)).
- `Text`, `Heading`, `ItemGroup` : Update the line height from 1.2 to 1.4 ([#60041](https://github.com/WordPress/gutenberg/pull/60041)).
+- `Autocomplete` : match the autocomplete styling to that of List View and Command Palette([#60131](https://github.com/WordPress/gutenberg/pull/60131)).
### Deprecation
diff --git a/packages/components/package.json b/packages/components/package.json
index 188e214e79bd45..a2768c887c37ab 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/components",
- "version": "27.3.0",
+ "version": "27.4.0",
"description": "UI components for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -76,8 +76,7 @@
"react-colorful": "^5.3.1",
"remove-accents": "^0.5.0",
"use-lilius": "^2.0.5",
- "uuid": "^9.0.1",
- "valtio": "1.7.0"
+ "uuid": "^9.0.1"
},
"peerDependencies": {
"react": "^18.0.0",
diff --git a/packages/components/src/autocomplete/style.scss b/packages/components/src/autocomplete/style.scss
index 4d04b3b8b52cf2..fdb29fe577f206 100644
--- a/packages/components/src/autocomplete/style.scss
+++ b/packages/components/src/autocomplete/style.scss
@@ -1,6 +1,6 @@
.components-autocomplete__popover .components-popover__content {
- padding: $grid-unit-20;
- min-width: 220px;
+ padding: $grid-unit-10;
+ min-width: 200px;
}
.components-autocomplete__result.components-button {
@@ -10,7 +10,13 @@
text-align: left;
width: 100%;
- &.is-selected {
- box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $components-color-accent;
+ &:focus:not(:disabled) {
+ @include block-toolbar-button-style__focus();
+ }
+
+ &.is-selected,
+ &:not(:disabled,[aria-disabled="true"]):active {
+ background: $components-color-accent;
+ color: $white;
}
}
diff --git a/packages/components/src/box-control/test/index.tsx b/packages/components/src/box-control/test/index.tsx
index 1ea3c84aae9225..681e7721d0c13a 100644
--- a/packages/components/src/box-control/test/index.tsx
+++ b/packages/components/src/box-control/test/index.tsx
@@ -80,6 +80,28 @@ describe( 'BoxControl', () => {
expect( input ).toHaveValue( '50' );
expect( screen.getByRole( 'slider' ) ).toHaveValue( '50' );
} );
+
+ it( 'should render the number input with a default min value of 0', () => {
+ render( {} } /> );
+
+ const input = screen.getByRole( 'textbox', { name: 'All sides' } );
+
+ expect( input ).toHaveAttribute( 'min', '0' );
+ } );
+
+ it( 'should pass down `inputProps` to the underlying number input', () => {
+ render(
+ {} }
+ inputProps={ { min: 10, max: 50 } }
+ />
+ );
+
+ const input = screen.getByRole( 'textbox', { name: 'All sides' } );
+
+ expect( input ).toHaveAttribute( 'min', '10' );
+ expect( input ).toHaveAttribute( 'max', '50' );
+ } );
} );
describe( 'Reset', () => {
diff --git a/packages/components/src/card/test/__snapshots__/index.tsx.snap b/packages/components/src/card/test/__snapshots__/index.tsx.snap
index 0b723732e3dbaf..60dcd306f3f526 100644
--- a/packages/components/src/card/test/__snapshots__/index.tsx.snap
+++ b/packages/components/src/card/test/__snapshots__/index.tsx.snap
@@ -8,8 +8,8 @@ Snapshot Diff:
@@ -1,8 +1,8 @@
@@ -25,8 +25,8 @@ Snapshot Diff:
@@ -1,8 +1,8 @@
@@ -42,8 +42,8 @@ Snapshot Diff:
@@ -1,8 +1,8 @@