From a9887c7e016f45bc1a666b68593566544bd98f22 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 6 Oct 2026 16:11:45 +0800 Subject: [PATCH 1/7] Editor: Add responsive viewport status to the footer Editor: Remove the footer viewport infotip Editor: Show viewport status for resized Desktop canvases Editor: Share device labels and icons between viewport controls Editor: Add independent footer viewport status preferences Editor: Preserve footer height without block breadcrumbs Editor: Keep the footer visible when either preference is enabled UI: Support in-document and current-only breadcrumb trails Block Editor: Collapse overflowing block breadcrumbs into a menu Simplify CHANGELOG Move footer JSX filename exceptions to ESLint suppressions Remove breadcrumb migration extracted to #84295 Editor: Use one preference for footer visibility --- packages/editor/CHANGELOG.md | 1 + .../editor/src/components/commands/index.js | 8 +- .../src/components/editor-interface/index.jsx | 29 ++++-- .../components/editor-interface/style.scss | 7 ++ .../test/index.jsdom.test.jsx | 90 ++++++++++++++++++ .../components/preferences-modal/index.jsx | 12 +-- .../src/components/preview-dropdown/index.jsx | 20 ++-- .../src/components/viewport-status/index.jsx | 73 ++++++++++++++ .../src/components/viewport-status/style.scss | 20 ++++ .../test/index.browser.test.jsx | 95 +++++++++++++++++++ .../src/components/visual-editor/index.jsx | 11 ++- packages/editor/src/store/private-actions.js | 13 +++ .../editor/src/store/private-selectors.js | 15 +++ packages/editor/src/store/reducer.js | 16 ++++ packages/editor/src/style.scss | 1 + packages/editor/src/utils/device-type.js | 9 ++ tools/eslint/suppressions.json | 15 +++ 17 files changed, 404 insertions(+), 31 deletions(-) create mode 100644 packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx create mode 100644 packages/editor/src/components/viewport-status/index.jsx create mode 100644 packages/editor/src/components/viewport-status/style.scss create mode 100644 packages/editor/src/components/viewport-status/test/index.browser.test.jsx diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index f0a8d1d2fb4d0d..c7a200e5aab2d5 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -24,6 +24,7 @@ ### Enhancements - `trashPost`: Accept a `force` option to delete the post permanently instead of moving it to the trash ([#84045](https://github.com/WordPress/gutenberg/pull/84045)). +- Show viewport dimensions and responsive editing status in the footer, and use the existing breadcrumb preference to show or hide the entire footer. - `PostAuthor`: The field renders with the `SearchableSelect` component of `@wordpress/ui` instead of `ComboboxControl` and `SelectControl` ([#83323](https://github.com/WordPress/gutenberg/pull/83323)). - `DocumentOutline`: Show the outline items' focus ring with the design system's outline instead of a legacy box-shadow ([#83755](https://github.com/WordPress/gutenberg/pull/83755)). - Notes: Add a "Notes" submenu to the View group of the Options menu ([#83768](https://github.com/WordPress/gutenberg/pull/83768)). diff --git a/packages/editor/src/components/commands/index.js b/packages/editor/src/components/commands/index.js index 2652d7048c008f..dd29060c1c33c7 100644 --- a/packages/editor/src/components/commands/index.js +++ b/packages/editor/src/components/commands/index.js @@ -264,16 +264,16 @@ const getEditorCommandLoader = () => commands.push( { name: 'core/toggle-breadcrumbs', label: showBlockBreadcrumbs - ? __( 'Hide block breadcrumbs' ) - : __( 'Show block breadcrumbs' ), + ? __( 'Hide editor footer' ) + : __( 'Show editor footer' ), category: 'command', callback: ( { close } ) => { toggle( 'core', 'showBlockBreadcrumbs' ); close(); createInfoNotice( showBlockBreadcrumbs - ? __( 'Breadcrumbs hidden.' ) - : __( 'Breadcrumbs visible.' ), + ? __( 'Editor footer hidden.' ) + : __( 'Editor footer visible.' ), { id: 'core/editor/toggle-breadcrumbs/notice', type: 'snackbar', diff --git a/packages/editor/src/components/editor-interface/index.jsx b/packages/editor/src/components/editor-interface/index.jsx index 5c34fdb69981fd..ece2655182873f 100644 --- a/packages/editor/src/components/editor-interface/index.jsx +++ b/packages/editor/src/components/editor-interface/index.jsx @@ -14,6 +14,7 @@ import { useState, useCallback, useLayoutEffect } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; import { InlineNotices } from '@wordpress/notices'; import { ThemeProvider } from '@wordpress/theme'; +import { Stack } from '@wordpress/ui'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import TemplateValidationNotice from '../template-validation-notice'; @@ -31,6 +32,7 @@ import SavePublishPanels from '../save-publish-panels'; import TextEditor from '../text-editor'; import VisualEditor from '../visual-editor'; import StylesCanvas from '../styles-canvas'; +import ViewportStatus from '../viewport-status'; const { blockToolbarShowPlaceholderKey } = unlock( blockEditorPrivateApis ); @@ -85,6 +87,7 @@ export default function EditorInterface( { isDistractionFree, isPreviewMode, showBlockBreadcrumbs, + hasCanvasWidth, postTypeLabel, stylesPath, showStylebook, @@ -103,6 +106,7 @@ export default function EditorInterface( { getShowStylebook, isRevisionsMode: _isRevisionsMode, isShowingRevisionDiff, + getCanvasWidth, } = unlock( select( editorStore ) ); const editorSettings = getEditorSettings(); @@ -123,6 +127,7 @@ export default function EditorInterface( { isDistractionFree: get( 'core', 'distractionFree' ), isPreviewMode: editorSettings.isPreviewMode, showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), + hasCanvasWidth: getCanvasWidth() !== undefined, postTypeLabel: getPostTypeLabel(), stylesPath: getStylesPath(), showStylebook: getShowStylebook(), @@ -283,15 +288,23 @@ export default function EditorInterface( { ! isDistractionFree && isLargeViewport && showBlockBreadcrumbs && + shouldShowBlockEditor && mode === 'visual' && ( - + + + { hasCanvasWidth && } + ) } actions={ diff --git a/packages/editor/src/components/editor-interface/style.scss b/packages/editor/src/components/editor-interface/style.scss index 196c3b27e4c466..b2186873598cc8 100644 --- a/packages/editor/src/components/editor-interface/style.scss +++ b/packages/editor/src/components/editor-interface/style.scss @@ -4,6 +4,13 @@ flex: 1 1 auto; height: var(--wpds-dimension-size-md); padding: 0 ($grid-unit-15 + 6px); + min-width: 0; +} + +.editor-editor-interface__footer { + flex: 1; + min-width: 0; + min-height: var(--wpds-dimension-size-md); } .editor-editor-interface .entities-saved-states__panel-header { diff --git a/packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx b/packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx new file mode 100644 index 00000000000000..eff5c3e2144afd --- /dev/null +++ b/packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx @@ -0,0 +1,90 @@ +import { beforeEach, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useViewportMatch } from '@wordpress/compose'; +import { store as preferencesStore } from '@wordpress/preferences'; +import { lock } from '../../../lock-unlock'; +import EditorInterface from '../'; + +vi.hoisted( () => globalThis.wpVitest.mockMatchMedia() ); +vi.mock( import( '@wordpress/data' ), { spy: true } ); +vi.mock( import( '@wordpress/compose' ), async ( importOriginal ) => ( { + ...( await importOriginal() ), + useViewportMatch: vi.fn(), +} ) ); +vi.mock( import( '@wordpress/interface' ), async ( importOriginal ) => ( { + ...( await importOriginal() ), + InterfaceSkeleton: ( { footer } ) => footer &&
{ footer }
, + ComplementaryArea: { Slot: () => null }, +} ) ); +vi.mock( import( '@wordpress/block-editor' ), async ( importOriginal ) => ( { + ...( await importOriginal() ), + BlockBreadcrumb: () => Breadcrumbs, +} ) ); +vi.mock( import( '../../viewport-status' ), () => ( { + default: () => Viewport status, +} ) ); +vi.mock( + import( '../../collaborators-presence/use-collaborator-notifications' ), + () => ( { + useCollaboratorNotifications: () => {}, + } ) +); + +beforeEach( () => { + vi.mocked( useViewportMatch ).mockReturnValue( true ); + const actions = { setIsListViewOpened: vi.fn() }; + lock( actions, { setShowRevisionDiff: vi.fn() } ); + vi.mocked( useDispatch ).mockReturnValue( actions ); +} ); + +it.each( [ + [ true, 781 ], + [ false, 781 ], + [ true, undefined ], + [ false, undefined ], +] )( + 'shows or hides the entire footer with the breadcrumb preference %s and canvas width %s', + ( showBlockBreadcrumbs, canvasWidth ) => { + const editorSelectors = { + getEditorSettings: () => ( { richEditingEnabled: true } ), + getEditorMode: () => 'visual', + getPostTypeLabel: () => 'Post', + getCurrentPostType: () => 'post', + getCurrentPostId: () => 1, + isInserterOpened: () => false, + isListViewOpened: () => false, + }; + lock( editorSelectors, { + getStylesPath: () => '/', + getShowStylebook: () => false, + isRevisionsMode: () => false, + isShowingRevisionDiff: () => false, + getCanvasWidth: () => canvasWidth, + } ); + vi.mocked( useSelect ).mockImplementation( ( mapSelect ) => + mapSelect( ( store ) => + store === preferencesStore + ? { + get: ( scope, name ) => + ( { + showBlockBreadcrumbs, + } )[ name ], + } + : editorSelectors + ) + ); + const hasViewportStatus = + showBlockBreadcrumbs && canvasWidth !== undefined; + render( ); + expect( !! screen.queryByText( 'Breadcrumbs' ) ).toBe( + showBlockBreadcrumbs + ); + expect( !! screen.queryByText( 'Viewport status' ) ).toBe( + hasViewportStatus + ); + expect( !! screen.queryByRole( 'contentinfo' ) ).toBe( + showBlockBreadcrumbs + ); + } +); diff --git a/packages/editor/src/components/preferences-modal/index.jsx b/packages/editor/src/components/preferences-modal/index.jsx index 835deb61b06940..0db6de51cc04d6 100644 --- a/packages/editor/src/components/preferences-modal/index.jsx +++ b/packages/editor/src/components/preferences-modal/index.jsx @@ -47,7 +47,7 @@ export default function EditorPreferencesModal( { extraSections = {} } ) { function PreferencesModalContents( { extraSections = {} } ) { const isLargeViewport = useViewportMatch( 'medium' ); - const { showBlockBreadcrumbsOption, showCollaborationOptions } = useSelect( + const { showFooterOptions, showCollaborationOptions } = useSelect( ( select ) => { const { getEditorSettings, isCollaborationEnabledForCurrentPost } = unlock( select( editorStore ) ); @@ -55,7 +55,7 @@ function PreferencesModalContents( { extraSections = {} } ) { const isRichEditingEnabled = getEditorSettings().richEditingEnabled; const isDistractionFreeEnabled = get( 'core', 'distractionFree' ); return { - showBlockBreadcrumbsOption: + showFooterOptions: ! isDistractionFreeEnabled && isLargeViewport && isRichEditingEnabled, @@ -89,14 +89,14 @@ function PreferencesModalContents( { extraSections = {} } ) { ) } label={ __( 'Always open List View' ) } /> - { showBlockBreadcrumbsOption && ( + { showFooterOptions && ( ) } { + const { getDeviceType, getCanvasWidth, getCanvasHeight } = unlock( + select( editorStore ) + ); + return { + deviceType: getDeviceType(), + canvasWidth: getCanvasWidth(), + canvasHeight: getCanvasHeight(), + isResponsiveEditing: unlock( + select( blockEditorStore ) + ).isResponsiveEditing(), + }; + }, [] ); + + if ( canvasWidth === undefined ) { + return null; + } + + const { label, icon } = DEVICE_TYPE_METADATA[ deviceType ]; + const dimensions = + typeof canvasHeight === 'number' + ? sprintf( + // translators: 1: Canvas width in pixels. 2: Canvas height in pixels. + __( '%1$d × %2$d' ), + Math.round( canvasWidth ), + Math.round( canvasHeight ) + ) + : sprintf( + // translators: %d: Canvas width in pixels. "auto" means the height is unconstrained. + __( '%d × auto' ), + Math.round( canvasWidth ) + ); + + return ( + + { isResponsiveEditing && ( + + { __( 'Responsive styles' ) } + + ) } + + + { sprintf( + // translators: 1: Viewport type, e.g. "Tablet". 2: Canvas dimensions, e.g. "800 × 400". + __( '%1$s (%2$s)' ), + label, + dimensions + ) } + + + ); +} diff --git a/packages/editor/src/components/viewport-status/style.scss b/packages/editor/src/components/viewport-status/style.scss new file mode 100644 index 00000000000000..ad3582ad464085 --- /dev/null +++ b/packages/editor/src/components/viewport-status/style.scss @@ -0,0 +1,20 @@ +@use "@wordpress/base-styles/variables" as *; + +.editor-viewport-status { + flex-shrink: 0; + white-space: nowrap; + margin-left: auto; + padding: 0 ( $grid-unit-15 + 6px ); + color: var(--wpds-color-foreground-content-neutral); + font-size: $default-font-size; + + .editor-viewport-status__responsive-styles { + // Match the responsive styles cue in the View menu. + color: var(--wp-block-synced-color); + } + + .editor-viewport-status__responsive-styles + .editor-viewport-status__device { + border-left: $border-width solid var(--wpds-color-stroke-surface-neutral); + padding-left: $grid-unit-15; + } +} diff --git a/packages/editor/src/components/viewport-status/test/index.browser.test.jsx b/packages/editor/src/components/viewport-status/test/index.browser.test.jsx new file mode 100644 index 00000000000000..4d0798330c2811 --- /dev/null +++ b/packages/editor/src/components/viewport-status/test/index.browser.test.jsx @@ -0,0 +1,95 @@ +import { beforeEach, expect, it } from 'vitest'; +import { render } from 'vitest-browser-react'; +import { page } from 'vitest/browser'; +import { createRegistry, RegistryProvider } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as editorStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; +import ViewportStatus from '../'; +import '../style.scss'; + +/** @type {ReturnType} */ +let registry; + +beforeEach( () => { + registry = createRegistry(); + registry.register( coreStore ); + registry.register( blockEditorStore ); + registry.register( editorStore ); + unlock( registry.dispatch( blockEditorStore ) ).resetZoomLevel(); + unlock( registry.dispatch( editorStore ) ).setCanvasWidth( 781 ); + unlock( registry.dispatch( editorStore ) ).setCanvasHeight( 640 ); +} ); + +it( 'updates viewport dimensions when the canvas changes size', async () => { + await render( + + + + ); + await expect + .element( page.getByText( 'Tablet (781 × 640)' ) ) + .toBeVisible(); + + const actions = unlock( registry.dispatch( editorStore ) ); + actions.setCanvasWidth( 400.4 ); + actions.setCanvasHeight( 600.6 ); + await expect + .element( page.getByText( 'Mobile (400 × 601)' ) ) + .toBeVisible(); + + actions.setCanvasHeight( undefined ); + await expect + .element( page.getByText( 'Mobile (400 × auto)' ) ) + .toBeVisible(); +} ); + +it( 'shows responsive editing status when enabled and removes it when disabled', async () => { + await render( + + + + ); + await expect + .element( page.getByText( 'Responsive styles', { exact: true } ) ) + .not.toBeInTheDocument(); + + const actions = unlock( registry.dispatch( blockEditorStore ) ); + actions.setResponsiveEditing( true ); + await expect + .element( page.getByText( 'Responsive styles', { exact: true } ) ) + .toBeVisible(); + actions.setResponsiveEditing( false ); + await expect + .element( page.getByText( 'Responsive styles', { exact: true } ) ) + .not.toBeInTheDocument(); + await expect + .element( page.getByText( 'Tablet (781 × 640)' ) ) + .toBeVisible(); +} ); + +it( 'shows Desktop dimensions for a resized canvas and hides status at full width and while zoomed out', async () => { + await render( + + + + ); + const status = page.getByText( 'Tablet (781 × 640)' ); + await expect.element( status ).toBeVisible(); + + const actions = unlock( registry.dispatch( editorStore ) ); + actions.setCanvasWidth( 1200 ); + await expect.element( status ).not.toBeInTheDocument(); + const desktopStatus = page.getByText( 'Desktop (1200 × 640)' ); + await expect.element( desktopStatus ).toBeVisible(); + actions.setCanvasWidth( undefined ); + await expect.element( desktopStatus ).not.toBeInTheDocument(); + actions.setCanvasWidth( 781 ); + await expect.element( status ).toBeVisible(); + + unlock( registry.dispatch( blockEditorStore ) ).setZoomLevel( 50 ); + await expect.element( status ).not.toBeInTheDocument(); + unlock( registry.dispatch( blockEditorStore ) ).resetZoomLevel(); + await expect.element( status ).toBeVisible(); +} ); diff --git a/packages/editor/src/components/visual-editor/index.jsx b/packages/editor/src/components/visual-editor/index.jsx index 307449e20c964c..ed4924bfb2210c 100644 --- a/packages/editor/src/components/visual-editor/index.jsx +++ b/packages/editor/src/components/visual-editor/index.jsx @@ -8,7 +8,7 @@ import { privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { useEffect, useRef, useMemo, useState } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { parse } from '@wordpress/blocks'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -404,6 +404,15 @@ function VisualEditor( { ? getCanvasHeight( canvasWidth, containerSize ) : '100%'; + const { setCanvasHeight } = unlock( useDispatch( editorStore ) ); + useEffect( () => { + setCanvasHeight( + typeof canvasHeight === 'number' ? canvasHeight : undefined + ); + }, [ canvasHeight, setCanvasHeight ] ); + // The store must not retain dimensions for a canvas that is no longer mounted. + useEffect( () => () => setCanvasHeight( undefined ), [ setCanvasHeight ] ); + // A resizable canvas (device preview) or a scaled one (zoom out) has no // margin. const hasCanvasMargin = ! isPreview && ! isZoomedOut && ! enableResizing; diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js index df9e906bdbbae9..3e9fdf2fab9c55 100644 --- a/packages/editor/src/store/private-actions.js +++ b/packages/editor/src/store/private-actions.js @@ -493,6 +493,19 @@ export function setCanvasWidth( width ) { }; } +/** + * Set the calculated canvas height for the footer viewport status. + * + * @param {number|undefined} height Canvas height in pixels, or undefined when unconstrained. + * @return {Object} Action object. + */ +export function setCanvasHeight( height ) { + return { + type: 'SET_CANVAS_HEIGHT', + height, + }; +} + /** * Set the current revision ID for revisions preview mode. * Pass a revision ID to enter revisions mode, or null to exit. diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js index 964f8d9d96ea0c..4281a416ac0d9a 100644 --- a/packages/editor/src/store/private-selectors.js +++ b/packages/editor/src/store/private-selectors.js @@ -322,6 +322,21 @@ export const getCanvasWidth = createRegistrySelector( } ); +/** + * Get the calculated canvas height published by the visual editor. + * + * @param {Object} state Global application state. + * @return {number|undefined} Canvas height in pixels, or undefined when unconstrained or zoomed out. + */ +export const getCanvasHeight = createRegistrySelector( + ( select ) => ( state ) => { + if ( unlock( select( blockEditorStore ) ).isZoomOut() ) { + return undefined; + } + return state.canvasHeight; + } +); + /** * Returns the current revisions page number. * diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 46aa0c361b73a7..42cfb667ca4557 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -284,6 +284,21 @@ export function canvasWidth( state = undefined, action ) { return state; } +/** + * Reducer for the calculated canvas height. + * + * @param {number|undefined} state Current state. + * @param {Object} action Dispatched action. + * @return {number|undefined} Updated state. + */ +export function canvasHeight( state = undefined, action ) { + switch ( action.type ) { + case 'SET_CANVAS_HEIGHT': + return action.height; + } + return state; +} + /** * Reducer storing the list of all programmatically removed panels. * @@ -492,6 +507,7 @@ export default combineReducers( { postAutosavingLock, renderingMode, canvasWidth, + canvasHeight, removedPanels, blockInserterPanel, inserterSidebarToggleRef, diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index a5bc385f59d88b..afc44834d063f9 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -72,5 +72,6 @@ @use "./components/text-editor/style.scss" as *; @use "./components/upload-progress-snackbar/style.scss" as *; @use "./components/visual-editor/style.scss" as *; +@use "./components/viewport-status/style.scss" as *; @use "./dataviews/fields/content-preview/style.scss" as *; @use "./hooks/push-changes-to-global-styles/style.scss" as *; diff --git a/packages/editor/src/utils/device-type.js b/packages/editor/src/utils/device-type.js index 11d9683ba4b1c4..055d86ded20014 100644 --- a/packages/editor/src/utils/device-type.js +++ b/packages/editor/src/utils/device-type.js @@ -1,4 +1,6 @@ import { privateApis as globalStylesEnginePrivateApis } from '@wordpress/global-styles-engine'; +import { __ } from '@wordpress/i18n'; +import { desktop, tablet, mobile } from '@wordpress/icons'; import { unlock } from '../lock-unlock'; const { getViewportBreakpoints, getViewportBreakpointValueInPixels } = unlock( @@ -15,6 +17,13 @@ const TABLET_DEVICE_TYPE = 'Tablet'; const MOBILE_DEVICE_TYPE = 'Mobile'; const DEVICE_PREVIEW_WIDTH_OFFSET = 1; +/** Labels and icons shared by device preview controls and viewport status. */ +export const DEVICE_TYPE_METADATA = { + Desktop: { label: __( 'Desktop' ), icon: desktop }, + Tablet: { label: __( 'Tablet' ), icon: tablet }, + Mobile: { label: __( 'Mobile' ), icon: mobile }, +}; + /** * Maps a device preview type to its corresponding viewport style state. Used * when Responsive editing is enabled so the device preview drives which diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json index 6493987cd5aa2c..6620ae39253484 100644 --- a/tools/eslint/suppressions.json +++ b/tools/eslint/suppressions.json @@ -6145,6 +6145,11 @@ "count": 1 } }, + "packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx": { + "react/jsx-filename-extension": { + "count": 1 + } + }, "packages/editor/src/components/editor-notices/index.jsx": { "react/jsx-filename-extension": { "count": 1 @@ -7178,6 +7183,16 @@ "count": 1 } }, + "packages/editor/src/components/viewport-status/index.jsx": { + "react/jsx-filename-extension": { + "count": 1 + } + }, + "packages/editor/src/components/viewport-status/test/index.browser.test.jsx": { + "react/jsx-filename-extension": { + "count": 1 + } + }, "packages/editor/src/components/visual-editor/edit-template-blocks-notification.jsx": { "react/jsx-filename-extension": { "count": 1 From 39e15f5eacae4ef644fa442b85d08ed74f4f3453 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 8 Oct 2026 14:40:29 +0800 Subject: [PATCH 2/7] Editor: Link footer changelog to #84146 --- packages/editor/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index c7a200e5aab2d5..ccf565435f54a4 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -9,6 +9,7 @@ ### Enhancements - Migrate the classic-theme template selector to `@wordpress/ui` `SelectControl` ([#84345](https://github.com/WordPress/gutenberg/pull/84345)). +- Show viewport dimensions and responsive editing status in the footer, and use the existing breadcrumb preference to show or hide the entire footer ([#84146](https://github.com/WordPress/gutenberg/pull/84146)). ### Bug Fixes @@ -24,7 +25,6 @@ ### Enhancements - `trashPost`: Accept a `force` option to delete the post permanently instead of moving it to the trash ([#84045](https://github.com/WordPress/gutenberg/pull/84045)). -- Show viewport dimensions and responsive editing status in the footer, and use the existing breadcrumb preference to show or hide the entire footer. - `PostAuthor`: The field renders with the `SearchableSelect` component of `@wordpress/ui` instead of `ComboboxControl` and `SelectControl` ([#83323](https://github.com/WordPress/gutenberg/pull/83323)). - `DocumentOutline`: Show the outline items' focus ring with the design system's outline instead of a legacy box-shadow ([#83755](https://github.com/WordPress/gutenberg/pull/83755)). - Notes: Add a "Notes" submenu to the View group of the Options menu ([#83768](https://github.com/WordPress/gutenberg/pull/83768)). From 5ea75ff42c0f6a8dabd31b0fecc4c505b86822b2 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 8 Oct 2026 15:48:23 +0800 Subject: [PATCH 3/7] Editor: Cover footer visibility in preferences e2e test --- .../test/index.jsdom.test.jsx | 90 ------------------- .../specs/editor/various/preferences.spec.js | 40 ++++++++- tools/eslint/suppressions.json | 5 -- 3 files changed, 38 insertions(+), 97 deletions(-) delete mode 100644 packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx diff --git a/packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx b/packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx deleted file mode 100644 index eff5c3e2144afd..00000000000000 --- a/packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import { beforeEach, expect, it, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { useViewportMatch } from '@wordpress/compose'; -import { store as preferencesStore } from '@wordpress/preferences'; -import { lock } from '../../../lock-unlock'; -import EditorInterface from '../'; - -vi.hoisted( () => globalThis.wpVitest.mockMatchMedia() ); -vi.mock( import( '@wordpress/data' ), { spy: true } ); -vi.mock( import( '@wordpress/compose' ), async ( importOriginal ) => ( { - ...( await importOriginal() ), - useViewportMatch: vi.fn(), -} ) ); -vi.mock( import( '@wordpress/interface' ), async ( importOriginal ) => ( { - ...( await importOriginal() ), - InterfaceSkeleton: ( { footer } ) => footer &&
{ footer }
, - ComplementaryArea: { Slot: () => null }, -} ) ); -vi.mock( import( '@wordpress/block-editor' ), async ( importOriginal ) => ( { - ...( await importOriginal() ), - BlockBreadcrumb: () => Breadcrumbs, -} ) ); -vi.mock( import( '../../viewport-status' ), () => ( { - default: () => Viewport status, -} ) ); -vi.mock( - import( '../../collaborators-presence/use-collaborator-notifications' ), - () => ( { - useCollaboratorNotifications: () => {}, - } ) -); - -beforeEach( () => { - vi.mocked( useViewportMatch ).mockReturnValue( true ); - const actions = { setIsListViewOpened: vi.fn() }; - lock( actions, { setShowRevisionDiff: vi.fn() } ); - vi.mocked( useDispatch ).mockReturnValue( actions ); -} ); - -it.each( [ - [ true, 781 ], - [ false, 781 ], - [ true, undefined ], - [ false, undefined ], -] )( - 'shows or hides the entire footer with the breadcrumb preference %s and canvas width %s', - ( showBlockBreadcrumbs, canvasWidth ) => { - const editorSelectors = { - getEditorSettings: () => ( { richEditingEnabled: true } ), - getEditorMode: () => 'visual', - getPostTypeLabel: () => 'Post', - getCurrentPostType: () => 'post', - getCurrentPostId: () => 1, - isInserterOpened: () => false, - isListViewOpened: () => false, - }; - lock( editorSelectors, { - getStylesPath: () => '/', - getShowStylebook: () => false, - isRevisionsMode: () => false, - isShowingRevisionDiff: () => false, - getCanvasWidth: () => canvasWidth, - } ); - vi.mocked( useSelect ).mockImplementation( ( mapSelect ) => - mapSelect( ( store ) => - store === preferencesStore - ? { - get: ( scope, name ) => - ( { - showBlockBreadcrumbs, - } )[ name ], - } - : editorSelectors - ) - ); - const hasViewportStatus = - showBlockBreadcrumbs && canvasWidth !== undefined; - render( ); - expect( !! screen.queryByText( 'Breadcrumbs' ) ).toBe( - showBlockBreadcrumbs - ); - expect( !! screen.queryByText( 'Viewport status' ) ).toBe( - hasViewportStatus - ); - expect( !! screen.queryByRole( 'contentinfo' ) ).toBe( - showBlockBreadcrumbs - ); - } -); diff --git a/test/e2e/specs/editor/various/preferences.spec.js b/test/e2e/specs/editor/various/preferences.spec.js index 5598a2d9188e06..311ede084aee7a 100644 --- a/test/e2e/specs/editor/various/preferences.spec.js +++ b/test/e2e/specs/editor/various/preferences.spec.js @@ -6,11 +6,47 @@ test.describe( 'Preferences', () => { } ); test.afterEach( async ( { requestUtils } ) => { - // Reset preferences via REST so the dismissed-sidebar preference - // doesn't leak into other tests that expect it open by default. + // Reset preferences via REST so interface visibility preferences + // don't leak into other tests that expect the defaults. await requestUtils.resetPreferences(); } ); + test( 'shows and hides the editor footer from preferences', async ( { + page, + } ) => { + await page.getByRole( 'button', { name: 'View', exact: true } ).click(); + await page.getByRole( 'menuitemradio', { name: 'Tablet' } ).click(); + await page.keyboard.press( 'Escape' ); + + const footer = page.getByRole( 'region', { name: 'Editor footer' } ); + const breadcrumbs = footer.getByRole( 'list', { + name: 'Block breadcrumb', + } ); + const viewportStatus = footer.getByText( /Tablet \(\d+ × \d+\)/ ); + await expect( breadcrumbs ).toBeVisible(); + await expect( viewportStatus ).toBeVisible(); + + for ( const showFooter of [ false, true ] ) { + await page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + await page.getByRole( 'menuitem', { name: 'Preferences' } ).click(); + const preferences = page.getByRole( 'dialog', { + name: 'Preferences', + } ); + await preferences + .getByRole( 'checkbox', { name: 'Show editor footer' } ) + .setChecked( showFooter ); + await preferences.getByRole( 'button', { name: 'Close' } ).click(); + + await expect( breadcrumbs ).toBeVisible( { visible: showFooter } ); + await expect( viewportStatus ).toBeVisible( { + visible: showFooter, + } ); + } + } ); + test( 'remembers sidebar dismissal between sessions', async ( { editor, page, diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json index 6620ae39253484..59c6ede2e3820b 100644 --- a/tools/eslint/suppressions.json +++ b/tools/eslint/suppressions.json @@ -6145,11 +6145,6 @@ "count": 1 } }, - "packages/editor/src/components/editor-interface/test/index.jsdom.test.jsx": { - "react/jsx-filename-extension": { - "count": 1 - } - }, "packages/editor/src/components/editor-notices/index.jsx": { "react/jsx-filename-extension": { "count": 1 From dfe59440e95db4d3b4f74fc862ee4f2eda089ced Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 12 Oct 2026 14:45:26 +0800 Subject: [PATCH 4/7] Editor: Hide responsive style indicators on the default viewport --- .../src/components/preview-dropdown/index.jsx | 3 ++- .../test/index.browser.test.tsx | 25 +++++++++++++++++++ .../src/components/viewport-status/index.jsx | 2 +- .../test/index.browser.test.jsx | 12 +++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/preview-dropdown/index.jsx b/packages/editor/src/components/preview-dropdown/index.jsx index 32018072e8fc1a..9e9a247ced032a 100644 --- a/packages/editor/src/components/preview-dropdown/index.jsx +++ b/packages/editor/src/components/preview-dropdown/index.jsx @@ -161,7 +161,8 @@ function PreviewMenu( { forceIsAutosaveable, disabled } ) { render={