{ children }
{ notices.map( ( notice ) => {
diff --git a/packages/components/src/snackbar/stories/list.story.tsx b/packages/components/src/snackbar/stories/list.story.tsx
index 5a759ddc661bfa..8548ffba1e1f09 100644
--- a/packages/components/src/snackbar/stories/list.story.tsx
+++ b/packages/components/src/snackbar/stories/list.story.tsx
@@ -70,7 +70,6 @@ Default.args = {
},
],
content: 'Post published.',
- isDismissible: true,
explicitDismiss: false,
},
{
@@ -83,7 +82,6 @@ Default.args = {
},
],
content: 'Post updated.',
- isDismissible: true,
explicitDismiss: false,
},
{
@@ -91,7 +89,6 @@ Default.args = {
spokenMessage: 'All content copied.',
actions: [],
content: 'All content copied.',
- isDismissible: true,
explicitDismiss: false,
},
],
diff --git a/packages/components/src/snackbar/test/index.tsx b/packages/components/src/snackbar/test/index.tsx
new file mode 100644
index 00000000000000..6be4e58c928123
--- /dev/null
+++ b/packages/components/src/snackbar/test/index.tsx
@@ -0,0 +1,267 @@
+/**
+ * External dependencies
+ */
+import { render, screen, within } from '@testing-library/react';
+import { click } from '@ariakit/test';
+
+/**
+ * WordPress dependencies
+ */
+import { speak } from '@wordpress/a11y';
+import { SVG, Path } from '@wordpress/primitives';
+
+/**
+ * Internal dependencies
+ */
+import Snackbar from '../index';
+
+jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn() } ) );
+const mockedSpeak = jest.mocked( speak );
+
+describe( 'Snackbar', () => {
+ const testId = 'snackbar';
+
+ beforeEach( () => {
+ mockedSpeak.mockReset();
+ } );
+
+ it( 'should render correctly', () => {
+ render( Message );
+
+ const snackbar = screen.getByTestId( testId );
+
+ expect( snackbar ).toBeVisible();
+ expect( snackbar ).toHaveTextContent( 'Message' );
+ } );
+
+ it( 'should render with an additional className', () => {
+ render( Message );
+
+ expect( screen.getByTestId( testId ) ).toHaveClass( 'gutenberg' );
+ } );
+
+ it( 'should render with an icon', () => {
+ const testIcon = (
+
+ );
+
+ render( Message );
+
+ const snackbar = screen.getByTestId( testId );
+ const icon = within( snackbar ).getByTestId( 'icon' );
+
+ expect( icon ).toBeVisible();
+ } );
+
+ it( 'should be dismissible by clicking the snackbar', async () => {
+ const onRemove = jest.fn();
+ const onDismiss = jest.fn();
+
+ render(
+
+ Message
+
+ );
+
+ const snackbar = screen.getByTestId( testId );
+
+ expect( snackbar ).toHaveAttribute( 'role', 'button' );
+ expect( snackbar ).toHaveAttribute(
+ 'aria-label',
+ 'Dismiss this notice'
+ );
+
+ await click( snackbar );
+
+ expect( onRemove ).toHaveBeenCalledTimes( 1 );
+ expect( onDismiss ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'should not be dismissible by clicking the snackbar when the `explicitDismiss` prop is set to `true`', async () => {
+ const onRemove = jest.fn();
+ const onDismiss = jest.fn();
+
+ render(
+
+ Message
+
+ );
+
+ const snackbar = screen.getByTestId( testId );
+
+ expect( snackbar ).not.toHaveAttribute( 'role', 'button' );
+ expect( snackbar ).not.toHaveAttribute(
+ 'aria-label',
+ 'Dismiss this notice'
+ );
+ expect( snackbar ).toHaveClass(
+ 'components-snackbar-explicit-dismiss'
+ );
+
+ await click( snackbar );
+
+ expect( onRemove ).not.toHaveBeenCalled();
+ expect( onDismiss ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should be dismissible by clicking the close button when the `explicitDismiss` prop is set to `true`', async () => {
+ const onRemove = jest.fn();
+ const onDismiss = jest.fn();
+
+ render(
+
+ Message
+
+ );
+
+ const snackbar = screen.getByTestId( testId );
+ const closeButton = within( snackbar ).getByRole( 'button', {
+ name: 'Dismiss this notice',
+ } );
+
+ await click( closeButton );
+
+ expect( onRemove ).toHaveBeenCalledTimes( 1 );
+ expect( onDismiss ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ describe( 'actions', () => {
+ it( 'should render only the first action with a warning when multiple actions are passed', () => {
+ render(
+
+ Message
+
+ );
+
+ expect( console ).toHaveWarnedWith(
+ 'Snackbar can only have one action. Use Notice if your message requires many actions.'
+ );
+
+ const snackbar = screen.getByTestId( testId );
+ const action = within( snackbar ).getByRole( 'link' );
+
+ expect( action ).toBeVisible();
+ expect( action ).toHaveTextContent( 'One' );
+ } );
+
+ it( 'should be rendered as a link when the `url` prop is set', () => {
+ render(
+
+ Post updated.
+
+ );
+
+ const snackbar = screen.getByTestId( testId );
+ const link = within( snackbar ).getByRole( 'link', {
+ name: 'View post',
+ } );
+
+ expect( link ).toHaveAttribute( 'href', 'https://example.com' );
+ } );
+
+ it( 'should be rendered as a button and call `onClick` when the `onClick` prop is set', async () => {
+ const onClick = jest.fn();
+
+ render(
+
+ Post updated.
+
+ );
+
+ const snackbar = screen.getByTestId( testId );
+ const button = within( snackbar ).getByRole( 'button', {
+ name: 'View post',
+ } );
+
+ await click( button );
+
+ expect( onClick ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'should be rendered as a link when the `url` prop and the `onClick` are set', () => {
+ render(
+ {},
+ },
+ ] }
+ >
+ Post updated.
+
+ );
+
+ const snackbar = screen.getByTestId( testId );
+ const link = within( snackbar ).getByRole( 'link', {
+ name: 'View post',
+ } );
+ expect( link ).toBeVisible();
+ } );
+ } );
+
+ describe( 'useSpokenMessage', () => {
+ it( 'should speak the given message', () => {
+ render( FYI );
+
+ expect( speak ).toHaveBeenCalledWith( 'FYI', 'polite' );
+ } );
+
+ it( 'should speak the given message by explicit politeness', () => {
+ render( Uh oh! );
+
+ expect( speak ).toHaveBeenCalledWith( 'Uh oh!', 'assertive' );
+ } );
+
+ it( 'should coerce a message to a string', () => {
+ // This test assumes that `@wordpress/a11y` is capable of handling
+ // markup strings appropriately.
+ render(
+
+ With emphasis this time.
+
+ );
+
+ expect( speak ).toHaveBeenCalledWith(
+ 'With emphasis this time.',
+ 'polite'
+ );
+ } );
+
+ it( 'should not re-speak an effectively equivalent element message', () => {
+ const { rerender } = render(
+
+ With emphasis this time.
+
+ );
+ rerender(
+
+ With emphasis this time.
+
+ );
+
+ expect( speak ).toHaveBeenCalledTimes( 1 );
+ } );
+ } );
+} );
diff --git a/packages/components/src/snackbar/test/list.tsx b/packages/components/src/snackbar/test/list.tsx
new file mode 100644
index 00000000000000..1b3749d42c61ef
--- /dev/null
+++ b/packages/components/src/snackbar/test/list.tsx
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import { click } from '@ariakit/test';
+
+/**
+ * Internal dependencies
+ */
+import SnackbarList from '../list';
+
+window.scrollTo = jest.fn();
+
+describe( 'SnackbarList', () => {
+ afterEach( () => {
+ jest.resetAllMocks();
+ } );
+
+ it( 'should get focus after a snackbar is dismissed', async () => {
+ render(
+ {} }
+ />
+ );
+
+ await click(
+ screen.getAllByRole( 'button', {
+ name: 'Dismiss this notice',
+ } )[ 0 ]
+ );
+
+ expect( screen.getByTestId( 'snackbar-list' ) ).toHaveFocus();
+ } );
+} );
diff --git a/packages/components/src/snackbar/types.ts b/packages/components/src/snackbar/types.ts
index 539c4c3ebdf65e..13ed98ade8aa7a 100644
--- a/packages/components/src/snackbar/types.ts
+++ b/packages/components/src/snackbar/types.ts
@@ -6,7 +6,11 @@ import type { MutableRefObject, ReactNode } from 'react';
/**
* Internal dependencies
*/
-import type { NoticeProps, NoticeChildren } from '../notice/types';
+import type {
+ NoticeProps,
+ NoticeChildren,
+ NoticeAction,
+} from '../notice/types';
type SnackbarOnlyProps = {
/**
@@ -28,8 +32,32 @@ type SnackbarOnlyProps = {
listRef?: MutableRefObject< HTMLDivElement | null >;
};
-export type SnackbarProps = Omit< NoticeProps, '__unstableHTML' > &
- SnackbarOnlyProps;
+export type SnackbarProps = Pick<
+ NoticeProps,
+ | 'className'
+ | 'children'
+ | 'spokenMessage'
+ | 'onRemove'
+ | 'politeness'
+ | 'onDismiss'
+> &
+ SnackbarOnlyProps & {
+ /**
+ * An array of action objects. Each member object should contain:
+ *
+ * - `label`: `string` containing the text of the button/link
+ * - `url`: `string` OR `onClick`: `( event: SyntheticEvent ) => void` to specify
+ * what the action does.
+ *
+ * The default appearance of an action button is inferred based on whether
+ * `url` or `onClick` are provided, rendering the button as a link if
+ * appropriate. If both props are provided, `url` takes precedence, and the
+ * action button will render as an anchor tag.
+ *
+ * @default []
+ */
+ actions?: Pick< NoticeAction, 'label' | 'url' | 'onClick' >[];
+ };
export type SnackbarListProps = {
notices: Array<
diff --git a/packages/create-block-interactive-template/README.md b/packages/create-block-interactive-template/README.md
index 9ae25fba2a1417..4417c647495c4c 100644
--- a/packages/create-block-interactive-template/README.md
+++ b/packages/create-block-interactive-template/README.md
@@ -10,7 +10,7 @@ This block template can be used by running the following command:
npx @wordpress/create-block --template @wordpress/create-block-interactive-template
```
-It requires Gutenberg 17.5 or higher.
+It requires at least WordPress 6.5 or Gutenberg 17.7.
## Contributing to this package
diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js
index 7fdc9331a2474b..979c3127b9ed52 100644
--- a/packages/data/src/redux-store/index.js
+++ b/packages/data/src/redux-store/index.js
@@ -64,12 +64,16 @@ const mapValues = ( obj, callback ) =>
] )
);
-// Convert Map objects to plain objects
-const mapToObject = ( key, state ) => {
+// Convert non serializable types to plain objects
+const devToolsReplacer = ( key, state ) => {
if ( state instanceof Map ) {
return Object.fromEntries( state );
}
+ if ( state instanceof window.HTMLElement ) {
+ return null;
+ }
+
return state;
};
@@ -421,7 +425,7 @@ function instantiateReduxStore( key, options, registry, thunkArgs ) {
name: key,
instanceId: key,
serialize: {
- replacer: mapToObject,
+ replacer: devToolsReplacer,
},
} )
);
diff --git a/packages/e2e-test-utils-playwright/src/request-utils/index.ts b/packages/e2e-test-utils-playwright/src/request-utils/index.ts
index 5036f3d0e8a97c..f6818945e16936 100644
--- a/packages/e2e-test-utils-playwright/src/request-utils/index.ts
+++ b/packages/e2e-test-utils-playwright/src/request-utils/index.ts
@@ -93,7 +93,7 @@ class RequestUtils {
},
} );
- const requestUtils = new RequestUtils( requestContext, {
+ const requestUtils = new this( requestContext, {
user,
storageState,
storageStatePath,
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php
index 0b8e6e1012d1a4..d1a7aa9211f105 100644
--- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php
@@ -15,6 +15,13 @@
array( 'clientNavigationDisabled' => true )
);
}
+
+if ( isset( $attributes['data'] ) ) {
+ wp_interactivity_state(
+ 'router',
+ array( 'data' => $attributes['data'] )
+ );
+}
?>
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js
index 1e137969936a09..b2d4ad0dc1ddeb 100644
--- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js
@@ -6,14 +6,23 @@ import { store } from '@wordpress/interactivity';
const { state } = store( 'router', {
state: {
status: 'idle',
- navigations: 0,
+ navigations: {
+ pending: 0,
+ count: 0,
+ },
timeout: 10000,
+ data: {
+ get getterProp() {
+ return `value from getter (${ state.data.prop1 })`;
+ }
+ }
},
actions: {
*navigate( e ) {
e.preventDefault();
- state.navigations += 1;
+ state.navigations.count += 1;
+ state.navigations.pending += 1;
state.status = 'busy';
const force = e.target.dataset.forceNavigation === 'true';
@@ -24,9 +33,9 @@ const { state } = store( 'router', {
);
yield actions.navigate( e.target.href, { force, timeout } );
- state.navigations -= 1;
+ state.navigations.pending -= 1;
- if ( state.navigations === 0 ) {
+ if ( state.navigations.pending === 0 ) {
state.status = 'idle';
}
},
diff --git a/packages/edit-post/src/components/editor-initialization/index.js b/packages/edit-post/src/components/editor-initialization/index.js
index 0ba9cc863f473f..bf61d569fc81c8 100644
--- a/packages/edit-post/src/components/editor-initialization/index.js
+++ b/packages/edit-post/src/components/editor-initialization/index.js
@@ -10,11 +10,10 @@ import {
* Data component used for initializing the editor and re-initializes
* when postId changes or on unmount.
*
- * @param {number} postId The id of the post.
* @return {null} This is a data component so does not render any ui.
*/
-export default function EditorInitialization( { postId } ) {
- useBlockSelectionListener( postId );
- useUpdatePostLinkListener( postId );
+export default function EditorInitialization() {
+ useBlockSelectionListener();
+ useUpdatePostLinkListener();
return null;
}
diff --git a/packages/edit-post/src/components/editor-initialization/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/listener-hooks.js
index 73872b4d7110e4..a5534135f9cfb8 100644
--- a/packages/edit-post/src/components/editor-initialization/listener-hooks.js
+++ b/packages/edit-post/src/components/editor-initialization/listener-hooks.js
@@ -19,24 +19,19 @@ import {
/**
* This listener hook monitors for block selection and triggers the appropriate
* sidebar state.
- *
- * @param {number} postId The current post id.
*/
-export const useBlockSelectionListener = ( postId ) => {
+export const useBlockSelectionListener = () => {
const { hasBlockSelection, isEditorSidebarOpened, isDistractionFree } =
- useSelect(
- ( select ) => {
- const { get } = select( preferencesStore );
- return {
- hasBlockSelection:
- !! select( blockEditorStore ).getBlockSelectionStart(),
- isEditorSidebarOpened:
- select( STORE_NAME ).isEditorSidebarOpened(),
- isDistractionFree: get( 'core', 'distractionFree' ),
- };
- },
- [ postId ]
- );
+ useSelect( ( select ) => {
+ const { get } = select( preferencesStore );
+ return {
+ hasBlockSelection:
+ !! select( blockEditorStore ).getBlockSelectionStart(),
+ isEditorSidebarOpened:
+ select( STORE_NAME ).isEditorSidebarOpened(),
+ isDistractionFree: get( 'core', 'distractionFree' ),
+ };
+ }, [] );
const { openGeneralSidebar } = useDispatch( STORE_NAME );
@@ -49,21 +44,24 @@ export const useBlockSelectionListener = ( postId ) => {
} else {
openGeneralSidebar( 'edit-post/document' );
}
- }, [ hasBlockSelection, isEditorSidebarOpened ] );
+ }, [
+ hasBlockSelection,
+ isDistractionFree,
+ isEditorSidebarOpened,
+ openGeneralSidebar,
+ ] );
};
/**
* This listener hook monitors any change in permalink and updates the view
* post link in the admin bar.
- *
- * @param {number} postId
*/
-export const useUpdatePostLinkListener = ( postId ) => {
+export const useUpdatePostLinkListener = () => {
const { newPermalink } = useSelect(
( select ) => ( {
newPermalink: select( editorStore ).getCurrentPost().link,
} ),
- [ postId ]
+ []
);
const nodeToUpdate = useRef();
@@ -71,7 +69,7 @@ export const useUpdatePostLinkListener = ( postId ) => {
nodeToUpdate.current =
document.querySelector( VIEW_AS_PREVIEW_LINK_SELECTOR ) ||
document.querySelector( VIEW_AS_LINK_SELECTOR );
- }, [ postId ] );
+ }, [] );
useEffect( () => {
if ( ! newPermalink || ! nodeToUpdate.current ) {
diff --git a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js
index 5087d303fafe1e..b9fd01cd5a2242 100644
--- a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js
+++ b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js
@@ -88,14 +88,14 @@ describe( 'listener hook tests', () => {
} );
describe( 'useBlockSelectionListener', () => {
const registry = createRegistry( mockStores );
- const TestComponent = ( { postId } ) => {
- useBlockSelectionListener( postId );
+ const TestComponent = () => {
+ useBlockSelectionListener();
return null;
};
const TestedOutput = () => {
return (
-
+
);
};
@@ -177,14 +177,14 @@ describe( 'listener hook tests', () => {
describe( 'useUpdatePostLinkListener', () => {
const registry = createRegistry( mockStores );
- const TestComponent = ( { postId } ) => {
- useUpdatePostLinkListener( postId );
+ const TestComponent = () => {
+ useUpdatePostLinkListener();
return null;
};
- const TestedOutput = ( { postId = 10 } ) => {
+ const TestedOutput = () => {
return (
-
+
);
};
@@ -222,7 +222,7 @@ describe( 'listener hook tests', () => {
} );
const { rerender } = render( );
- rerender( );
+ rerender( );
expect( mockSelector ).toHaveBeenCalledTimes( 1 );
act( () => {
diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js
index f179ee6156e63e..fd44d3dae4ca4a 100644
--- a/packages/edit-post/src/editor.js
+++ b/packages/edit-post/src/editor.js
@@ -96,7 +96,7 @@ function Editor( {
>
-
+
diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js
index b9fe954e79ad55..fd0e730945ece1 100644
--- a/packages/edit-post/src/store/selectors.js
+++ b/packages/edit-post/src/store/selectors.js
@@ -538,7 +538,7 @@ export const isEditingTemplate = createRegistrySelector( ( select ) => () => {
since: '6.5',
alternative: `select( 'core/editor' ).getRenderingMode`,
} );
- return select( editorStore ).getCurrentPostType() !== 'post-only';
+ return select( editorStore ).getCurrentPostType() === 'wp_template';
} );
/**
diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js
index 01bc4cdfa2ddfc..a2b146d8dc95db 100644
--- a/packages/edit-site/src/components/block-editor/editor-canvas.js
+++ b/packages/edit-site/src/components/block-editor/editor-canvas.js
@@ -26,10 +26,9 @@ import {
const { EditorCanvas: EditorCanvasRoot } = unlock( editorPrivateApis );
function EditorCanvas( { enableResizing, settings, children, ...props } ) {
- const { hasBlocks, isFocusMode, templateType, canvasMode, isZoomOutMode } =
- useSelect( ( select ) => {
- const { getBlockCount, __unstableGetEditorMode } =
- select( blockEditorStore );
+ const { hasBlocks, isFocusMode, templateType, canvasMode } = useSelect(
+ ( select ) => {
+ const { getBlockCount } = select( blockEditorStore );
const { getEditedPostType, getCanvasMode } = unlock(
select( editSiteStore )
);
@@ -38,11 +37,12 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) {
return {
templateType: _templateType,
isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ),
- isZoomOutMode: __unstableGetEditorMode() === 'zoom-out',
canvasMode: getCanvasMode(),
hasBlocks: !! getBlockCount(),
};
- }, [] );
+ },
+ []
+ );
const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
const [ isFocused, setIsFocused ] = useState( false );
@@ -52,8 +52,11 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) {
}
}, [ canvasMode ] );
- const viewModeProps = {
- 'aria-label': __( 'Editor Canvas' ),
+ // In view mode, make the canvas iframe be perceived and behave as a button
+ // to switch to edit mode, with a meaningful label and no title attribute.
+ const viewModeIframeProps = {
+ 'aria-label': __( 'Edit' ),
+ title: null,
role: 'button',
tabIndex: 0,
onFocus: () => setIsFocused( true ),
@@ -107,9 +110,7 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) {
renderAppender={ showBlockAppender }
styles={ styles }
iframeProps={ {
- expand: isZoomOutMode,
- scale: isZoomOutMode ? 0.45 : undefined,
- frameSize: isZoomOutMode ? 100 : undefined,
+ shouldZoom: true,
className: classnames(
'edit-site-visual-editor__editor-canvas',
{
@@ -117,7 +118,7 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) {
}
),
...props,
- ...( canvasMode === 'view' ? viewModeProps : {} ),
+ ...( canvasMode === 'view' ? viewModeIframeProps : {} ),
} }
>
{ children }
diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss
index 3d042f612f29ed..fa7fd7e13df3e4 100644
--- a/packages/edit-site/src/components/block-editor/style.scss
+++ b/packages/edit-site/src/components/block-editor/style.scss
@@ -22,7 +22,6 @@
position: relative;
height: 100%;
display: block;
- overflow: hidden;
background-color: $gray-300;
// Centralize the editor horizontally (flex-direction is column).
align-items: center;
@@ -62,8 +61,6 @@
.components-resizable-box__container {
margin: 0 auto;
- // Removing this will cancel the bottom margins in the iframe.
- overflow: auto;
}
&.is-view-mode {
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js
index 35276b0ad8b2b4..8da33192859f14 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js
@@ -162,16 +162,6 @@ function FontLibraryProvider( { children } ) {
// Demo
const [ loadedFontUrls ] = useState( new Set() );
- // Theme data
- const { site, currentTheme } = useSelect( ( select ) => {
- return {
- site: select( coreStore ).getSite(),
- currentTheme: select( coreStore ).getCurrentTheme(),
- };
- } );
- const themeUrl =
- site?.url + '/wp-content/themes/' + currentTheme?.stylesheet;
-
const getAvailableFontsOutline = ( availableFontFamilies ) => {
const outline = availableFontFamilies.reduce( ( acc, font ) => {
const availableFontFaces =
@@ -416,7 +406,7 @@ function FontLibraryProvider( { children } ) {
// If the font doesn't have a src, don't load it.
if ( ! fontFace.src ) return;
// Get the src of the font.
- const src = getDisplaySrcFromFontFace( fontFace.src, themeUrl );
+ const src = getDisplaySrcFromFontFace( fontFace.src );
// If the font is already loaded, don't load it again.
if ( ! src || loadedFontUrls.has( src ) ) return;
// Load the font in the browser.
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
index 583c19c7aeb967..f22a2f15693f85 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
@@ -100,7 +100,10 @@ function InstalledFonts() {
) }
-
+
+
{ notice && (
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
index 011f09b12a841f..1458b47cd010a3 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
@@ -121,7 +121,13 @@ export async function loadFontFaceInBrowser( fontFace, source, addTo = 'all' ) {
}
}
-export function getDisplaySrcFromFontFace( input, urlPrefix ) {
+/**
+ * Retrieves the display source from a font face src.
+ *
+ * @param {string|string[]} input - The font face src.
+ * @return {string|undefined} The display source or undefined if the input is invalid.
+ */
+export function getDisplaySrcFromFontFace( input ) {
if ( ! input ) {
return;
}
@@ -132,9 +138,9 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) {
} else {
src = input;
}
- // If it is a theme font, we need to make the url absolute
- if ( src.startsWith( 'file:.' ) && urlPrefix ) {
- src = src.replace( 'file:.', urlPrefix );
+ // It's expected theme fonts will already be loaded in the browser.
+ if ( src.startsWith( 'file:.' ) ) {
+ return;
}
if ( ! isUrlEncoded( src ) ) {
src = encodeURI( src );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js
index 9c6235443a0992..3cbdc0283f1a9e 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js
@@ -21,33 +21,22 @@ describe( 'getDisplaySrcFromFontFace', () => {
);
} );
- it( 'makes URL absolute when it starts with file:. and urlPrefix is given', () => {
- const input = 'file:./font1';
- const urlPrefix = 'http://example.com';
- expect( getDisplaySrcFromFontFace( input, urlPrefix ) ).toBe(
- 'http://example.com/font1'
- );
- } );
-
- it( 'does not modify URL if it does not start with file:.', () => {
- const input = [ 'http://some-other-place.com/font1' ];
- const urlPrefix = 'http://example.com';
- expect( getDisplaySrcFromFontFace( input, urlPrefix ) ).toBe(
- 'http://some-other-place.com/font1'
- );
+ it( 'return undefined when the url starts with file:', () => {
+ const input = 'file:./theme/assets/font1.ttf';
+ expect( getDisplaySrcFromFontFace( input ) ).toBe( undefined );
} );
it( 'encodes the URL if it is not encoded', () => {
- const input = 'file:./assets/font one with spaces.ttf';
+ const input = 'https://example.org/font one with spaces.ttf';
expect( getDisplaySrcFromFontFace( input ) ).toBe(
- 'file:./assets/font%20one%20with%20spaces.ttf'
+ 'https://example.org/font%20one%20with%20spaces.ttf'
);
} );
it( 'does not encode the URL if it is already encoded', () => {
- const input = 'file:./font%20one';
+ const input = 'https://example.org/fonts/font%20one.ttf';
expect( getDisplaySrcFromFontFace( input ) ).toBe(
- 'file:./font%20one'
+ 'https://example.org/fonts/font%20one.ttf'
);
} );
} );
diff --git a/packages/edit-site/src/components/global-styles/screen-typography.js b/packages/edit-site/src/components/global-styles/screen-typography.js
index f76dc6fb381004..40e2ab08320b75 100644
--- a/packages/edit-site/src/components/global-styles/screen-typography.js
+++ b/packages/edit-site/src/components/global-styles/screen-typography.js
@@ -9,7 +9,7 @@ import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
-import TypographyElements from './typogrphy-elements';
+import TypographyElements from './typography-elements';
import FontFamilies from './font-families';
import ScreenHeader from './header';
diff --git a/packages/edit-site/src/components/global-styles/typogrphy-elements.js b/packages/edit-site/src/components/global-styles/typography-elements.js
similarity index 100%
rename from packages/edit-site/src/components/global-styles/typogrphy-elements.js
rename to packages/edit-site/src/components/global-styles/typography-elements.js
diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js
index fcb0a74b0b3b88..d15be016173b03 100644
--- a/packages/edit-site/src/components/layout/index.js
+++ b/packages/edit-site/src/components/layout/index.js
@@ -70,6 +70,7 @@ export default function Layout() {
const {
isDistractionFree,
+ isZoomOutMode,
hasFixedToolbar,
hasBlockSelected,
canvasMode,
@@ -96,6 +97,9 @@ export default function Layout() {
'core',
'distractionFree'
),
+ isZoomOutMode:
+ select( blockEditorStore ).__unstableGetEditorMode() ===
+ 'zoom-out',
hasBlockSelected:
select( blockEditorStore ).getBlockSelectionStart(),
};
@@ -172,6 +176,7 @@ export default function Layout() {
'is-full-canvas': canvasMode === 'edit',
'has-fixed-toolbar': hasFixedToolbar,
'is-block-toolbar-visible': hasBlockSelected,
+ 'is-zoom-out': isZoomOutMode,
}
) }
>
diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js
index 9cf02dbd3e2ab4..ed6d60734c9cc8 100644
--- a/packages/edit-site/src/components/page-patterns/index.js
+++ b/packages/edit-site/src/components/page-patterns/index.js
@@ -210,6 +210,26 @@ function Title( { item, categoryId } ) {
}
return (
+
+ { item.type === PATTERN_TYPES.theme ? (
+ item.title
+ ) : (
+
+ ) }
+
{ itemIcon && ! isNonUserPattern && (
) }
-
- { item.type === PATTERN_TYPES.theme ? (
- item.title
- ) : (
-
- ) }
-
);
}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/edit-button.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/edit-button.js
deleted file mode 100644
index 962062a96da743..00000000000000
--- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/edit-button.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { pencil } from '@wordpress/icons';
-/**
- * Internal dependencies
- */
-import SidebarButton from '../sidebar-button';
-import { useLink } from '../routes/link';
-import { NAVIGATION_POST_TYPE } from '../../utils/constants';
-
-export default function EditButton( { postId } ) {
- const linkInfo = useLink( {
- postId,
- postType: NAVIGATION_POST_TYPE,
- canvas: 'edit',
- } );
- return (
-
- );
-}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js
index 960e0363f2e588..e6348531516f66 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js
@@ -10,7 +10,6 @@ import { SidebarNavigationScreenWrapper } from '../sidebar-navigation-screen-nav
import ScreenNavigationMoreMenu from './more-menu';
import NavigationMenuEditor from './navigation-menu-editor';
import buildNavigationLabel from '../sidebar-navigation-screen-navigation-menus/build-navigation-label';
-import EditButton from './edit-button';
export default function SingleNavigationMenu( {
navigationMenu,
@@ -30,7 +29,6 @@ export default function SingleNavigationMenu( {
onSave={ handleSave }
onDuplicate={ handleDuplicate }
/>
-
>
}
title={ buildNavigationLabel(
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js
index 2188fade413dbb..7c120133c674ee 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js
@@ -31,7 +31,7 @@ function TemplateDataviewItem( { template, isActive } ) {
export default function DataviewsTemplatesSidebarContent( {
activeView,
postType,
- config,
+ title,
} ) {
const { records } = useEntityRecords( 'postType', postType, {
per_page: -1,
@@ -54,7 +54,7 @@ export default function DataviewsTemplatesSidebarContent( {
}
/>
diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js
index 7cf049282618b3..aea839840dda99 100644
--- a/packages/edit-site/src/components/sidebar/index.js
+++ b/packages/edit-site/src/components/sidebar/index.js
@@ -74,7 +74,7 @@ function SidebarScreens() {
}
backPath="/page"
/>
diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss
index c86efc10eafb3c..8b6ba0093b4dc3 100644
--- a/packages/edit-site/src/style.scss
+++ b/packages/edit-site/src/style.scss
@@ -98,7 +98,7 @@ body.js.site-editor-php {
}
.interface-interface-skeleton__content {
- background-color: $gray-900;
+ background-color: $gray-300;
}
}
diff --git a/packages/editor/src/components/commands/index.js b/packages/editor/src/components/commands/index.js
index eb97e8d10b93a8..f1de29af690d01 100644
--- a/packages/editor/src/components/commands/index.js
+++ b/packages/editor/src/components/commands/index.js
@@ -25,9 +25,12 @@ function useEditorCommandLoader() {
isFocusMode,
isPreviewMode,
isViewable,
+ isCodeEditingEnabled,
+ isRichEditingEnabled,
} = useSelect( ( select ) => {
const { get } = select( preferencesStore );
- const { isListViewOpened, getCurrentPostType } = select( editorStore );
+ const { isListViewOpened, getCurrentPostType, getEditorSettings } =
+ select( editorStore );
const { getSettings } = select( blockEditorStore );
const { getPostType } = select( coreStore );
@@ -40,6 +43,8 @@ function useEditorCommandLoader() {
isTopToolbar: get( 'core', 'fixedToolbar' ),
isPreviewMode: getSettings().__unstableIsPreviewMode,
isViewable: getPostType( getCurrentPostType() )?.viewable ?? false,
+ isCodeEditingEnabled: getEditorSettings().codeEditingEnabled,
+ isRichEditingEnabled: getEditorSettings().richEditingEnabled,
};
}, [] );
const { toggle } = useDispatch( preferencesStore );
@@ -51,6 +56,7 @@ function useEditorCommandLoader() {
toggleDistractionFree,
} = useDispatch( editorStore );
const { getCurrentPostId } = useSelect( editorStore );
+ const allowSwitchEditorMode = isCodeEditingEnabled && isRichEditingEnabled;
if ( isPreviewMode ) {
return { commands: [], isLoading: false };
@@ -141,18 +147,20 @@ function useEditorCommandLoader() {
},
} );
- commands.push( {
- name: 'core/toggle-code-editor',
- label:
- editorMode === 'visual'
- ? __( 'Open code editor' )
- : __( 'Exit code editor' ),
- icon: code,
- callback: ( { close } ) => {
- switchEditorMode( editorMode === 'visual' ? 'text' : 'visual' );
- close();
- },
- } );
+ if ( allowSwitchEditorMode ) {
+ commands.push( {
+ name: 'core/toggle-code-editor',
+ label:
+ editorMode === 'visual'
+ ? __( 'Open code editor' )
+ : __( 'Exit code editor' ),
+ icon: code,
+ callback: ( { close } ) => {
+ switchEditorMode( editorMode === 'visual' ? 'text' : 'visual' );
+ close();
+ },
+ } );
+ }
commands.push( {
name: 'core/toggle-breadcrumbs',
diff --git a/packages/editor/src/components/provider/constants.js b/packages/editor/src/components/provider/constants.js
deleted file mode 100644
index a81b2fd37563af..00000000000000
--- a/packages/editor/src/components/provider/constants.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const PAGE_CONTENT_BLOCK_TYPES = [
- 'core/post-title',
- 'core/post-featured-image',
- 'core/post-content',
-];
diff --git a/packages/editor/src/components/provider/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/disable-non-page-content-blocks.js
index 48a8119350d78f..fd4722ebe40f4d 100644
--- a/packages/editor/src/components/provider/disable-non-page-content-blocks.js
+++ b/packages/editor/src/components/provider/disable-non-page-content-blocks.js
@@ -2,39 +2,46 @@
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
-import {
- useBlockEditingMode,
- store as blockEditorStore,
-} from '@wordpress/block-editor';
+import { store as blockEditorStore } from '@wordpress/block-editor';
import { useEffect } from '@wordpress/element';
-/**
- * Internal dependencies
- */
-import { PAGE_CONTENT_BLOCK_TYPES } from './constants';
+const PAGE_CONTENT_BLOCKS = [
+ 'core/post-title',
+ 'core/post-featured-image',
+ 'core/post-content',
+];
+
+function useDisableNonPageContentBlocks() {
+ const contentIds = useSelect( ( select ) => {
+ const { getBlocksByName, getBlockParents, getBlockName } =
+ select( blockEditorStore );
+ return getBlocksByName( PAGE_CONTENT_BLOCKS ).filter( ( clientId ) =>
+ getBlockParents( clientId ).every( ( parentClientId ) => {
+ const parentBlockName = getBlockName( parentClientId );
+ return (
+ parentBlockName !== 'core/query' &&
+ ! PAGE_CONTENT_BLOCKS.includes( parentBlockName )
+ );
+ } )
+ );
+ }, [] );
-function DisableBlock( { clientId } ) {
- const isDescendentOfQueryLoop = useSelect(
- ( select ) => {
- const { getBlockParentsByBlockName } = select( blockEditorStore );
- return (
- getBlockParentsByBlockName( clientId, 'core/query' ).length !==
- 0
- );
- },
- [ clientId ]
- );
- const mode = isDescendentOfQueryLoop ? undefined : 'contentOnly';
const { setBlockEditingMode, unsetBlockEditingMode } =
useDispatch( blockEditorStore );
+
useEffect( () => {
- if ( mode ) {
- setBlockEditingMode( clientId, mode );
- return () => {
- unsetBlockEditingMode( clientId );
- };
+ setBlockEditingMode( '', 'disabled' ); // Disable editing at the root level.
+
+ for ( const contentId of contentIds ) {
+ setBlockEditingMode( contentId, 'contentOnly' ); // Re-enable each content block.
}
- }, [ clientId, mode, setBlockEditingMode, unsetBlockEditingMode ] );
+ return () => {
+ unsetBlockEditingMode( '' );
+ for ( const contentId of contentIds ) {
+ unsetBlockEditingMode( contentId );
+ }
+ };
+ }, [ contentIds, setBlockEditingMode, unsetBlockEditingMode ] );
}
/**
@@ -42,14 +49,5 @@ function DisableBlock( { clientId } ) {
* page content to be edited.
*/
export default function DisableNonPageContentBlocks() {
- useBlockEditingMode( 'disabled' );
- const clientIds = useSelect( ( select ) => {
- return select( blockEditorStore ).getBlocksByName(
- PAGE_CONTENT_BLOCK_TYPES
- );
- }, [] );
-
- return clientIds.map( ( clientId ) => {
- return ;
- } );
+ useDisableNonPageContentBlocks();
}
diff --git a/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js
new file mode 100644
index 00000000000000..d76530828a7999
--- /dev/null
+++ b/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js
@@ -0,0 +1,90 @@
+/**
+ * External dependencies
+ */
+import { render } from '@testing-library/react';
+
+/**
+ * WordPress dependencies
+ */
+import { createRegistry, RegistryProvider } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import DisableNonPageContentBlocks from '../disable-non-page-content-blocks';
+
+describe( 'DisableNonPageContentBlocks', () => {
+ it( 'disables page content blocks', () => {
+ const testBlocks = {
+ 0: 'core/template-part',
+ /**/ '00': 'core/site-title',
+ /**/ '01': 'core/navigation',
+ 1: 'core/group',
+ /**/ 10: 'core/post-title',
+ /**/ 11: 'core/post-featured-image',
+ /**/ 12: 'core/post-content',
+ /**/ /**/ 120: 'core/paragraph',
+ /**/ /**/ 121: 'core/post-featured-image',
+ 2: 'core/query',
+ /**/ 20: 'core/post-title',
+ /**/ 21: 'core/post-featured-image',
+ /**/ 22: 'core/post-content',
+ 3: 'core/template-part',
+ /**/ 30: 'core/paragraph',
+ };
+
+ const setBlockEditingMode = jest.fn( () => ( {
+ type: 'SET_BLOCK_EDITING_MODE',
+ } ) );
+ const unsetBlockEditingMode = jest.fn( () => ( {
+ type: 'UNSET_BLOCK_EDITING_MODE',
+ } ) );
+
+ const registry = createRegistry( {
+ 'core/block-editor': {
+ reducer: () => {},
+ selectors: {
+ getBlocksByName( state, blockNames ) {
+ return Object.keys( testBlocks ).filter( ( clientId ) =>
+ blockNames.includes( testBlocks[ clientId ] )
+ );
+ },
+ getBlockParents( state, clientId ) {
+ return clientId.slice( 0, -1 ).split( '' );
+ },
+ getBlockName( state, clientId ) {
+ return testBlocks[ clientId ];
+ },
+ },
+ actions: {
+ setBlockEditingMode,
+ unsetBlockEditingMode,
+ },
+ },
+ } );
+
+ const { unmount } = render(
+
+
+
+ );
+
+ expect( setBlockEditingMode.mock.calls ).toEqual( [
+ [ '', 'disabled' ], // root
+ [ '10', 'contentOnly' ], // post-title
+ [ '11', 'contentOnly' ], // post-featured-image
+ [ '12', 'contentOnly' ], // post-content
+ // NOT the post-featured-image nested within post-content
+ // NOT any of the content blocks within query
+ ] );
+
+ unmount();
+
+ expect( unsetBlockEditingMode.mock.calls ).toEqual( [
+ [ '' ], // root
+ [ '10' ], // post-title
+ [ '11' ], // post-featured-image
+ [ '12' ], // post-content
+ ] );
+ } );
+} );
diff --git a/packages/element/README.md b/packages/element/README.md
index 5636fdda56a525..9ebf2a632d5019 100755
--- a/packages/element/README.md
+++ b/packages/element/README.md
@@ -247,7 +247,7 @@ This is the same concept as the React Native implementation.
_Related_
-- Here is an example of how to use the select method:
+- Here is an example of how to use the select method:
_Usage_
diff --git a/packages/element/src/platform.js b/packages/element/src/platform.js
index c646b6c86d51a2..841cd06e4cabb5 100644
--- a/packages/element/src/platform.js
+++ b/packages/element/src/platform.js
@@ -17,7 +17,7 @@ const Platform = {
*
* This is the same concept as the React Native implementation.
*
- * @see https://facebook.github.io/react-native/docs/platform-specific-code#platform-module
+ * @see https://reactnative.dev/docs/platform-specific-code#platform-module
*
* Here is an example of how to use the select method:
* @example
diff --git a/packages/html-entities/README.md b/packages/html-entities/README.md
index 211215c50a451b..1133c7df156194 100644
--- a/packages/html-entities/README.md
+++ b/packages/html-entities/README.md
@@ -23,6 +23,8 @@ Decodes the HTML entities from a given string.
_Usage_
```js
+import { decodeEntities } from '@wordpress/html-entities';
+
const result = decodeEntities( 'á' );
console.log( result ); // result will be "á"
```
diff --git a/packages/html-entities/src/index.js b/packages/html-entities/src/index.js
index 503d6f69daf82e..1df4ec41484bdd 100644
--- a/packages/html-entities/src/index.js
+++ b/packages/html-entities/src/index.js
@@ -8,6 +8,8 @@ let _decodeTextArea;
*
* @example
* ```js
+ * import { decodeEntities } from '@wordpress/html-entities';
+ *
* const result = decodeEntities( 'á' );
* console.log( result ); // result will be "á"
* ```
diff --git a/packages/interactivity-router/CHANGELOG.md b/packages/interactivity-router/CHANGELOG.md
index 72a9dd459a688c..799425e4cd9d51 100644
--- a/packages/interactivity-router/CHANGELOG.md
+++ b/packages/interactivity-router/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Bug Fixes
+
+- Fix navigate() issues related to initial state merges. ([#57134](https://github.com/WordPress/gutenberg/pull/57134))
+
## 1.2.0 (2024-02-21)
## 1.1.0 (2024-02-09)
diff --git a/packages/interactivity-router/src/index.js b/packages/interactivity-router/src/index.js
index 724a2660df41dc..03d399338167ce 100644
--- a/packages/interactivity-router/src/index.js
+++ b/packages/interactivity-router/src/index.js
@@ -3,10 +3,18 @@
*/
import { store, privateApis, getConfig } from '@wordpress/interactivity';
-const { directivePrefix, getRegionRootFragment, initialVdom, toVdom, render } =
- privateApis(
- 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.'
- );
+const {
+ directivePrefix,
+ getRegionRootFragment,
+ initialVdom,
+ toVdom,
+ render,
+ parseInitialData,
+ populateInitialData,
+ batch,
+} = privateApis(
+ 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.'
+);
// The cache of visited and prefetched pages.
const pages = new Map();
@@ -45,20 +53,24 @@ const regionsToVdom = ( dom, { vdom } = {} ) => {
: toVdom( region );
} );
const title = dom.querySelector( 'title' )?.innerText;
- return { regions, title };
+ const initialData = parseInitialData( dom );
+ return { regions, title, initialData };
};
// Render all interactive regions contained in the given page.
const renderRegions = ( page ) => {
- const attrName = `data-${ directivePrefix }-router-region`;
- document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => {
- const id = region.getAttribute( attrName );
- const fragment = getRegionRootFragment( region );
- render( page.regions[ id ], fragment );
+ batch( () => {
+ populateInitialData( page.initialData );
+ const attrName = `data-${ directivePrefix }-router-region`;
+ document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => {
+ const id = region.getAttribute( attrName );
+ const fragment = getRegionRootFragment( region );
+ render( page.regions[ id ], fragment );
+ } );
+ if ( page.title ) {
+ document.title = page.title;
+ }
} );
- if ( page.title ) {
- document.title = page.title;
- }
};
/**
@@ -176,7 +188,11 @@ export const { state, actions } = store( 'core/router', {
// out, and let the newer execution to update the HTML.
if ( navigatingTo !== href ) return;
- if ( page ) {
+ if (
+ page &&
+ ! page.initialData?.config?.[ 'core/router' ]
+ ?.clientNavigationDisabled
+ ) {
renderRegions( page );
window.history[
options.replace ? 'replaceState' : 'pushState'
diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md
index 8e48ead8429d3b..1e81760b8d05c1 100644
--- a/packages/interactivity/CHANGELOG.md
+++ b/packages/interactivity/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Bug Fixes
+
+- Prevent passing state proxies as receivers to deepSignal proxy handlers. ([#57134](https://github.com/WordPress/gutenberg/pull/57134))
+
## 5.1.0 (2024-02-21)
### Bug Fixes
diff --git a/packages/interactivity/README.md b/packages/interactivity/README.md
index 6c1e98d0115564..a6b2a7fb6f3dc0 100644
--- a/packages/interactivity/README.md
+++ b/packages/interactivity/README.md
@@ -17,7 +17,7 @@ These Core blocks are already powered by the API:
> **Note**
> This step is only required if you are using this API outside of WordPress.
>
-> Within WordPress, the package is already bundled in Core, so all you need to do to ensure it is loaded, by adding `wp-interactivity` to the dependency array of the module script.
+> Within WordPress, the package is already bundled in Core, so all you need to do to ensure it is loaded, by adding `@wordpress/interactivity` to the dependency array of the script module.
>
>This happens automatically when you use the dependency extraction Webpack plugin that is used in tools like wp-scripts.
diff --git a/packages/interactivity/docs/api-reference.md b/packages/interactivity/docs/api-reference.md
index b755b124646679..319a3974430140 100644
--- a/packages/interactivity/docs/api-reference.md
+++ b/packages/interactivity/docs/api-reference.md
@@ -1,52 +1,23 @@
# API Reference
-> **Note**
-> Interactivity API is only available for WordPress 6.5 and above.
+
+Interactivity API is only available for WordPress 6.5 and above.
+
-To add interactivity to blocks using the Interactivity API, developers can use:
+To add interactions to blocks using the Interactivity API, developers can use:
-- **Directives** - added to the markup to add specific behavior to the DOM elements of the block.
-- **Store** - that contains the logic and data (state, actions, or side effects, among others) needed for the behavior.
+- **Directives:** Added to the markup to add specific behavior to the DOM elements of the block
+- **Store:** Contains the logic and data (state, actions, side effects, etc.) needed for the behavior
DOM elements are connected to data stored in the state and context through directives. If data in the state or context change directives will react to those changes, updating the DOM accordingly (see [diagram](https://excalidraw.com/#json=T4meh6lltJh6TCX51NTIu,DmIhxYSGFTL_ywZFbsmuSw)).
![State & Directives](https://make.wordpress.org/core/files/2024/02/interactivity-state-directives.png)
-## Table of Contents
-
-- [The directives](#the-directives)
- - [List of Directives](#list-of-directives)
- - [`wp-interactive`](#wp-interactive) ![](https://img.shields.io/badge/DECLARATIVE-afd2e3.svg)
- - [`wp-context`](#wp-context) ![](https://img.shields.io/badge/STATE-afd2e3.svg)
- - [`wp-bind`](#wp-bind) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg)
- - [`wp-class`](#wp-class) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg)
- - [`wp-style`](#wp-style) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg)
- - [`wp-text`](#wp-text) ![](https://img.shields.io/badge/CONTENT-afd2e3.svg)
- - [`wp-on`](#wp-on) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg)
- - [`wp-on-window`](#wp-on-window) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg)
- - [`wp-on-document`](#wp-on-document) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg)
- - [`wp-watch`](#wp-watch) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg)
- - [`wp-init`](#wp-init) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg)
- - [`wp-run`](#wp-run) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg)
- - [`wp-key`](#wp-key) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg)
- - [`wp-each`](#wp-each) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg)
- - [Values of directives are references to store properties](#values-of-directives-are-references-to-store-properties)
-- [The store](#the-store)
- - [Elements of the store](#elements-of-the-store)
- - [State](#state)
- - [Actions](#actions)
- - [Side Effects](#side-effects)
- - [Setting the store](#setting-the-store)
- - [On the client side](#on-the-client-side)
- - [On the server side](#on-the-server-side)
-
-## The directives
+## What are directives?
Directives are custom attributes that are added to the markup of your block to add behavior to its DOM elements. This can be done in the `render.php` file (for dynamic blocks) or the `save.js` file (for static blocks).
-Interactivity API directives use the `data-` prefix.
-
-_Example of directives used in the HTML markup_
+Interactivity API directives use the `data-` prefix. Here's an example of directives used in HTML markup.
```html
I'm also interactive, and I can also use directives!
-```html
+
**Note**
> The use of `data-wp-interactive` is a requirement for the Interactivity API "engine" to work. In the following examples the `data-wp-interactive` has not been added for the sake of simplicity. Also, the `data-wp-interactive` directive will be injected automatically in the future.
-#### `wp-context`
+### `wp-context`
It provides a **local** state available to a specific HTML node and its children.
The `wp-context` directive accepts a stringified JSON as a value.
-_Example of `wp-context` directive_
-
```php
//render.php
@@ -135,9 +104,7 @@ store( "myPlugin", {
},
} );
```
-
-
Different contexts can be defined at different levels, and deeper levels will merge their own context with any parent one:
@@ -156,13 +123,9 @@ Different contexts can be defined at different levels, and deeper levels will me
```
-#### `wp-bind`
-
-It allows setting HTML attributes on elements based on a boolean or string value.
-
-> This directive follows the syntax `data-wp-bind--attribute`.
+### `wp-bind`
-_Example of `wp-bind` directive_
+This directive allows setting HTML attributes on elements based on a boolean or string value. It follows the syntax `data-wp-bind--attribute`.
```html
@@ -194,34 +157,28 @@ store( "myPlugin", {
},
} );
```
-
-
The `wp-bind` directive is executed:
-- When the element is created.
-- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference).
+- When the element is created
+- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference)
When `wp-bind` directive references a callback to get its final value:
- The `wp-bind` directive will be executed each time there's a change on any of the properties of the `state` or `context` used inside this callback.
- The returned value in the callback function is used to change the value of the associated attribute.
-The `wp-bind` will do different things over the DOM element is applied, depending on its value:
-
- - If the value is `true`, the attribute is added: `
`.
- - If the value is `false`, the attribute is removed: `
`.
- - If the value is a string, the attribute is added with its value assigned: `
`.
+The `wp-bind` will do different things when the DOM element is applied, depending on its value:
-#### `wp-class`
+ - If the value is `true`, the attribute is added: `
`
+ - If the value is `false`, the attribute is removed: `
`
+ - If the value is a string, the attribute is added with its value assigned: `
`
-It adds or removes a class to an HTML element, depending on a boolean value.
+### `wp-class`
-> This directive follows the syntax `data-wp-class--classname`.
-
-_Example of `wp-class` directive_
+This directive adds or removes a class to an HTML element, depending on a boolean value. It follows the syntax `data-wp-class--classname`.
```html
@@ -255,26 +212,20 @@ store( "myPlugin", {
}
} );
```
-
-
The `wp-class` directive is executed:
-- When the element is created.
-- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference).
+- When the element is created
+- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference)
When `wp-class` directive references a callback to get its final boolean value, the callback receives the class name: `className`.
The boolean value received by the directive is used to toggle (add when `true` or remove when `false`) the associated class name from the `class` attribute.
-#### `wp-style`
+### `wp-style`
-It adds or removes inline style to an HTML element, depending on its value.
-
-> This directive follows the syntax `data-wp-style--css-property`.
-
-_Example of `wp-style` directive_
+This directive adds or removes inline style to an HTML element, depending on its value. It follows the syntax `data-wp-style--css-property`.
```html
@@ -297,23 +248,21 @@ store( "myPlugin", {
},
} );
```
-
-
The `wp-style` directive is executed:
-- When the element is created.
-- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference).
+- When the element is created
+- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference)
When `wp-style` directive references a callback to get its final value, the callback receives the class style property: `css-property`.
-The value received by the directive is used to add or remove the style attribute with the associated CSS property: :
+The value received by the directive is used to add or remove the style attribute with the associated CSS property:
-- If the value is `false`, the style attribute is removed: `
`.
-- If the value is a string, the attribute is added with its value assigned: `
`.
+- If the value is `false`, the style attribute is removed: `
`
+- If the value is a string, the attribute is added with its value assigned: `
`
-#### `wp-text`
+### `wp-text`
It sets the inner text of an HTML element.
@@ -339,24 +288,18 @@ store( "myPlugin", {
},
} );
```
-
-
The `wp-text` directive is executed:
-- When the element is created.
-- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference).
+- When the element is created
+- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference)
The returned value is used to change the inner content of the element: `
value
`.
-#### `wp-on`
+### `wp-on`
-It runs code on dispatched DOM events like `click` or `keyup`.
-
-> The syntax of this directive is `data-wp-on--[event]` (like `data-wp-on--click` or `data-wp-on--keyup`).
-
-_Example of `wp-on` directive_
+This directive runs code on dispatched DOM events like `click` or `keyup`. The syntax is `data-wp-on--[event]` (like `data-wp-on--click` or `data-wp-on--keyup`).
```php