From a67f5be8ec63ffaef7052786c8c34681fe51891d Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Fri, 4 Sep 2026 19:24:30 -0300 Subject: [PATCH 01/14] Jetpack notices: back SimpleNotice with the @wordpress/ui Notice SimpleNotice and NoticeAction now wrap `Notice.Root` and its action subcomponents, so all 14 files that render a Jetpack admin notice move to the design system at once without touching their call sites. SimpleNotice keeps its two-slot contract: with `text` set, children are the actions and go into `Notice.Actions`; without it, children are the body. `NoticeAction` maps `href` to `Notice.ActionLink` with `external` becoming `openInNewTab`, which also draws the external-link arrow the Gridicon used to supply, and falls back to `Notice.ActionButton` when there is no href. The `dops-notice` classes are gone from the component, so the old rules cannot fight the design system's styles. Two consequences handled here: `AdminNotices` still rebuilds server-rendered core notices into that chassis with jQuery, so its stylesheet is now loaded from `scss/style.scss`; and the floating notice stack targets its children rather than a notice class, because `Notice.Root`'s CSS-module class name is hashed. Known gaps, none of which block the swap: `isCompact` has no design system equivalent, a Gridicon name passed as `icon` is dropped in favour of the intent's own icon, and `NoticeAction`'s `variant` and `icon` props are no longer honoured at their single call sites each. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017AnRQ7ZE68VGgGVQB4pFND --- .../components/global-notices/style.scss | 37 ++------- .../_inc/client/components/notice/index.jsx | 80 +++++++------------ .../components/notice/notice-action.jsx | 34 +++----- .../jetpack/_inc/client/scss/style.scss | 3 + .../update-simple-notice-wpds-wrapper | 4 + 5 files changed, 54 insertions(+), 104 deletions(-) create mode 100644 projects/plugins/jetpack/changelog/update-simple-notice-wpds-wrapper diff --git a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss index 37bd8777e067..d24e7cb3c41c 100644 --- a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss +++ b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss @@ -39,44 +39,21 @@ } } -.global-notices .dops-notice { - flex-wrap: nowrap; +/* + * Targets children rather than a notice class: the notices are now + * `@wordpress/ui` `Notice.Root`, whose CSS-module class name is hashed. + * The container is click-through so it never blocks the page, so each notice + * has to opt back in, and undo the right-alignment that stacks them. + */ +.global-notices > * { margin-bottom: 0; text-align: left; pointer-events: auto; - border-radius: 0; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2), 0 0 56px rgba(0, 0, 0, 0.15); - .dops-notice__icon-wrapper { - border-radius: 0; - } - @include calypso-mixins.breakpoint( ">660px" ) { - display: flex; - overflow: hidden; margin-bottom: 24px; - border-radius: 3px; - - .dops-notice__icon-wrapper { - border-radius: 3px 0 0 3px; - } - } -} - -.global-notices .dops-notice a.dops-notice__action { - - @include calypso-mixins.breakpoint( ">660px" ) { - font-size: 14px; - padding: 13px 16px; - } -} - -.global-notices .dops-notice__dismiss { - flex-shrink: 0; - - @include calypso-mixins.breakpoint( ">660px" ) { - padding: 13px 16px 0; } } diff --git a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx index d6dbf0ab2399..6c5a0996f5e5 100644 --- a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx @@ -1,10 +1,6 @@ -import clsx from 'clsx'; +import { Notice } from '@wordpress/ui'; import PropTypes from 'prop-types'; -import { Component } from 'react'; -import Gridicon from 'components/gridicon'; -import onKeyDownCallback from 'utils/onkeydown-callback'; - -import './style.scss'; +import { Component, isValidElement } from 'react'; const noop = () => {}; @@ -30,7 +26,7 @@ export default class SimpleNotice extends Component { PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), PropTypes.arrayOf( PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ) ), ] ), - icon: PropTypes.string, + icon: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), onDismissClick: PropTypes.func, className: PropTypes.string, display: PropTypes.bool, @@ -50,28 +46,19 @@ export default class SimpleNotice extends Component { } } - getIcon = () => { - let icon; - + getIntent = () => { switch ( this.props.status ) { case 'is-info': - icon = 'info'; - break; + return 'info'; case 'is-success': - icon = 'checkmark'; - break; + return 'success'; case 'is-error': - icon = 'notice'; - break; + return 'error'; case 'is-warning': - icon = 'notice'; - break; + return 'warning'; default: - icon = 'info'; - break; + return 'neutral'; } - - return icon; }; clearText = text => { @@ -89,41 +76,32 @@ export default class SimpleNotice extends Component { isCompact, onDismissClick, showDismiss = ! isCompact, // by default, show on normal notices, don't show on compact ones - status, text, dismissText, display, } = this.props; - const classes = clsx( 'dops-notice', status, className, { - 'is-compact': isCompact, - 'is-dismissable': showDismiss, - 'is-hidden': ! display, - } ); + + if ( ! display ) { + return null; + } + + // `text` marks the caller as using the two-slot form, where children are the + // actions. Without it, children are the body. + const body = text ? this.clearText( text ) : children; + const actions = text ? children : null; return ( -
- - - - - { text ? this.clearText( text ) : children } - - { text ? children : null } - { showDismiss && ( - - - - { dismissText } - - - ) } -
+ + { body } + { actions ? { actions } : null } + { showDismiss && } + ); } } diff --git a/projects/plugins/jetpack/_inc/client/components/notice/notice-action.jsx b/projects/plugins/jetpack/_inc/client/components/notice/notice-action.jsx index ffb22ad95ea8..51b121863ced 100644 --- a/projects/plugins/jetpack/_inc/client/components/notice/notice-action.jsx +++ b/projects/plugins/jetpack/_inc/client/components/notice/notice-action.jsx @@ -1,8 +1,6 @@ +import { Notice } from '@wordpress/ui'; import PropTypes from 'prop-types'; import { Component } from 'react'; -import Gridicon from 'components/gridicon'; - -import './style.scss'; export default class NoticeAction extends Component { static displayName = 'NoticeAction'; @@ -20,27 +18,17 @@ export default class NoticeAction extends Component { }; render() { - let className = 'dops-notice__action'; - if ( this.props.variant === 'secondary' ) { - className += ' is-secondary'; - } - - const attributes = { - className, - href: this.props.href, - onClick: this.props.onClick, - }; - - if ( this.props.external ) { - attributes.target = '_blank'; + const { children, href, onClick, external } = this.props; + + if ( href ) { + // `openInNewTab` also draws the external-link arrow the old Gridicon supplied. + return ( + + { children } + + ); } - return ( - - { this.props.children } - { this.props.icon && } - { this.props.external && } - - ); + return { children }; } } diff --git a/projects/plugins/jetpack/_inc/client/scss/style.scss b/projects/plugins/jetpack/_inc/client/scss/style.scss index 4f5fda4028b6..99fdf7600e77 100644 --- a/projects/plugins/jetpack/_inc/client/scss/style.scss +++ b/projects/plugins/jetpack/_inc/client/scss/style.scss @@ -38,6 +38,9 @@ @include meta.load-css( "../components/support-card/style" ); @include meta.load-css( "../components/forms/styles" ); @include meta.load-css( "../components/admin-notices/style" ); +// `AdminNotices` rebuilds core notices into the dops chassis with jQuery, +// so these rules outlive `SimpleNotice`'s move to `@wordpress/ui`. +@include meta.load-css( "../components/notice/style" ); @include meta.load-css( "../components/module-toggle/style" ); @include meta.load-css( "../components/navigation-settings/style" ); @include meta.load-css( "../components/settings-nav-tabs/style" ); diff --git a/projects/plugins/jetpack/changelog/update-simple-notice-wpds-wrapper b/projects/plugins/jetpack/changelog/update-simple-notice-wpds-wrapper new file mode 100644 index 000000000000..b6f9ee70f6e4 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-simple-notice-wpds-wrapper @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Notices: render Jetpack admin notices with the WordPress design system Notice component. From 51d0a72a4e4da54466aa2f563ec382f034ed2d24 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 7 Sep 2026 14:31:57 -0300 Subject: [PATCH 02/14] Jetpack notices: restore the spacing, underline and padding the class carried MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrapping SimpleNotice over `@wordpress/ui` dropped the `dops-notice` class, and three behaviours went with it, none of them visible in that diff. `SimpleNotice` now always contributes a `jp-notice` class alongside whatever the caller passes, because `Notice.Root`'s own class name is a CSS-module hash and page styles need something stable to target. The stacked notices lost their 24px gap, which `.dops-notice` supplied as `margin-bottom`. Both containers that stack notices use `Stack` instead: `JetpackNotices`, which was a bare `
`, and `JetpackConnectionErrors`, which returned a bare array. `gap="xl"` is the same 24px, applied as an inline style, so nothing on the page can override it. Links inside notices lost their underline to `.jetpack-pagestyles a`, which is unlayered and so beats every `@layer wp-ui` rule regardless of specificity; `Link` sets no `text-decoration` of its own and relies on the UA underline. A scoped counter-rule restores it, to be removed once the `@wordpress/ui` global CSS defense grows a `text-decoration` bridge. Inside a settings card the notices were laid out for the old chassis, which was a dark full-bleed banner with square corners. A rounded, bordered design system notice cannot sit flush against the card edge, so both here get the padding the rest of the card uses. Account protection's notices also move to real JSX children rather than a `children` prop, which any actual children would beat. The Like buttons notice moves out of its `SettingsGroup`. That group paints an 80% white scrim over its children in offline mode, which left the notice below readable contrast and its link under an overlay that takes pointer events — on a notice whose whole purpose is to explain why the controls are unavailable. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../components/jetpack-notices/index.jsx | 5 ++- .../jetpack-connection-errors.jsx | 7 +++- .../_inc/client/components/notice/index.jsx | 5 ++- .../components/settings-card/style.scss | 19 ++------- .../_inc/client/scss/shared/_main.scss | 9 ++++ .../client/security/account-protection.jsx | 42 ++++++++++--------- .../jetpack/_inc/client/sharing/likes.jsx | 9 ++-- 7 files changed, 54 insertions(+), 42 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/index.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/index.jsx index 4fab2e61cd1e..aeeed2dc8fa3 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/index.jsx @@ -1,6 +1,7 @@ import { getRedirectUrl } from '@automattic/jetpack-components'; import { createInterpolateElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; +import { Stack } from '@wordpress/ui'; import PropTypes from 'prop-types'; import { Component } from 'react'; import { connect } from 'react-redux'; @@ -190,7 +191,7 @@ class JetpackNotices extends Component { const isUserConnectScreen = this.props.location.pathname.startsWith( '/connect-user' ); return ( -
+ { this.props.siteConnectionStatus && this.props.userCanConnectSite && @@ -255,7 +256,7 @@ class JetpackNotices extends Component { linkText={ link.label } /> ) ) } -
+ ); } } diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/jetpack-connection-errors.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/jetpack-connection-errors.jsx index e08566458872..597b695fd0ac 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/jetpack-connection-errors.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/jetpack-connection-errors.jsx @@ -1,4 +1,5 @@ import { __ } from '@wordpress/i18n'; +import { Stack } from '@wordpress/ui'; import PropTypes from 'prop-types'; import { Component, Fragment } from 'react'; import SimpleNotice from 'components/notice'; @@ -128,6 +129,10 @@ export default class JetpackConnectionErrors extends Component { } } - return Object.values( errorsToDisplay ).map( error => this.renderOne( error ) ); + return ( + + { Object.values( errorsToDisplay ).map( error => this.renderOne( error ) ) } + + ); } } diff --git a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx index 6c5a0996f5e5..3ab864ece0fd 100644 --- a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx @@ -1,4 +1,5 @@ import { Notice } from '@wordpress/ui'; +import clsx from 'clsx'; import PropTypes from 'prop-types'; import { Component, isValidElement } from 'react'; @@ -93,7 +94,9 @@ export default class SimpleNotice extends Component { return ( { ! isSupported && ( - + { __( 'Learn more', 'jetpack' ) } - } - /> + +
) } { isSupported && ! isActive && ( - + { __( 'Learn about the risks', 'jetpack' ) } - } - /> + + ) } - { shouldShowLikeBlock && ( + + + { shouldShowLikeBlock && ( +
- ) } - +
+ ) } { shouldShowLikeBlock && configCard() } From 2de338581dd3d1f5369bdc52d0f61abc48ed12a0 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 7 Sep 2026 14:32:21 -0300 Subject: [PATCH 03/14] Jetpack notices: give SimpleNotice a title slot and use it where one exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@wordpress/ui` `Notice` renders a title as `heading-md` and a description as `body-md`. SimpleNotice only ever filled the description, so callers that wanted a heading built one by hand. A `title` prop feeds `Notice.Title`. Four call sites already had a title in their markup and now pass it instead: the deprecation notice drops an inline `fontWeight: 600` div and hands over the `title` prop it already accepted, the static warning splits its two server-substituted placeholders, and both SEO banners drop a ``. This is extraction only — no copy changes and no new translated strings. The remaining notices either carry a single sentence, where a title would have to be written, or take their text from the server, where it would have to be derived from an error code. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../components/jetpack-notices/deprecation-notice.jsx | 2 +- .../client/components/jetpack-notices/static-warning.jsx | 8 ++------ .../jetpack/_inc/client/components/notice/index.jsx | 3 +++ projects/plugins/jetpack/_inc/client/traffic/index.jsx | 8 ++++++-- projects/plugins/jetpack/_inc/client/traffic/seo.jsx | 8 ++++++-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/deprecation-notice.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/deprecation-notice.jsx index 76e31bac2de8..d1f399942aaf 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/deprecation-notice.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/deprecation-notice.jsx @@ -7,10 +7,10 @@ const DeprecationNotice = ( { dismissNotice, message, link, linkText, title } ) return ( - { title &&
{ title }
}
{ message }
{ link && ( diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/static-warning.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/static-warning.jsx index 6b304055f199..3cb9176aed5b 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/static-warning.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/static-warning.jsx @@ -11,12 +11,8 @@ export class StaticWarning extends Component { render() { return ( - -
- #HEADER_TEXT# -
- #TEXT# -
+ +
#TEXT#
); } diff --git a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx index 3ab864ece0fd..79efada0eb5e 100644 --- a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx @@ -27,6 +27,7 @@ export default class SimpleNotice extends Component { PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), PropTypes.arrayOf( PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ) ), ] ), + title: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), icon: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), onDismissClick: PropTypes.func, className: PropTypes.string, @@ -78,6 +79,7 @@ export default class SimpleNotice extends Component { onDismissClick, showDismiss = ! isCompact, // by default, show on normal notices, don't show on compact ones text, + title, dismissText, display, } = this.props; @@ -101,6 +103,7 @@ export default class SimpleNotice extends Component { // here, and the intent already picks a sensible default icon. icon={ isValidElement( icon ) ? icon : undefined } > + { title ? { title } : null } { body } { actions ? { actions } : null } { showDismiss && } diff --git a/projects/plugins/jetpack/_inc/client/traffic/index.jsx b/projects/plugins/jetpack/_inc/client/traffic/index.jsx index 888b5f1693b9..67630b95b3a7 100644 --- a/projects/plugins/jetpack/_inc/client/traffic/index.jsx +++ b/projects/plugins/jetpack/_inc/client/traffic/index.jsx @@ -108,9 +108,13 @@ export class Traffic extends Component { /> ) } { seoMovedToDashboard && foundMovedToDashboard && ( - +
- { __( 'Jetpack SEO has its own dashboard', 'jetpack' ) }

{ __( 'Manage your search engine optimization settings from the redesigned SEO dashboard.', diff --git a/projects/plugins/jetpack/_inc/client/traffic/seo.jsx b/projects/plugins/jetpack/_inc/client/traffic/seo.jsx index 49d7b3843f40..6b9d7f54a8dd 100644 --- a/projects/plugins/jetpack/_inc/client/traffic/seo.jsx +++ b/projects/plugins/jetpack/_inc/client/traffic/seo.jsx @@ -192,9 +192,13 @@ export const SEO = withModuleSettingsFormHelpers( ); return ( - +

- { __( 'SEO has a new home', 'jetpack' ) }

{ __( 'Manage all of your search engine optimization from the redesigned Jetpack SEO dashboard.', From a9963600f43bbe0988bff22fb317650cfc49984e Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 7 Sep 2026 15:19:48 -0300 Subject: [PATCH 04/14] Jetpack notices: return null from the notice components that render nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `JetpackStateNotices` and `DismissableNotices` each wrapped their output in a `

`, so both left an empty element in the notice stack whenever they had nothing to show. `DismissableNotices` always does: its `renderNotices` has only a `default: return false`. That was free while each notice carried its own `margin-bottom`, because an empty div has no margin. The stack is now a flex `Stack`, and `gap` applies between every child, empty or not — so a lone notice sat between two 24px bands. Both return null instead. The other siblings already returned false, which puts no node in the DOM. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../_inc/client/components/jetpack-notices/dismissable.jsx | 2 +- .../_inc/client/components/jetpack-notices/state-notices.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/dismissable.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/dismissable.jsx index 3347b6c10354..ba5b059cc2d6 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/dismissable.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/dismissable.jsx @@ -24,7 +24,7 @@ class DismissableNotices extends Component { }; render() { - return
{ this.renderNotices() }
; + return this.renderNotices() || null; } } diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/state-notices.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/state-notices.jsx index 39e9c0039d68..465378da7e6a 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/state-notices.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/state-notices.jsx @@ -288,7 +288,7 @@ class JetpackStateNotices extends Component { }; render() { - return
{ this.state.showNotice ? this.renderContent() : null }
; + return this.state.showNotice ? this.renderContent() ?? null : null; } } From 30ea3d6ea0c50f540ddf75d65890e345486bccdd Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 7 Sep 2026 15:46:27 -0300 Subject: [PATCH 05/14] Jetpack notices: stop the toasts collapsing on desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Notice.Root` sets `container-type: inline-size` for its own container query, which means it is sized as if it had no contents. Above 660px the toast stack is a fixed container with `left: auto` and no width, so it shrinks to fit — and a size-contained child contributes nothing. The container collapsed and the icon and text spilled outside the tinted box. The legacy notice was a plain flex element, so it fed that calculation. Turning containment off for the toasts restores it, with no width invented. The query this disables only applies to a notice with a title, which a toast never has. The `text-align` pair goes with it: that was how the legacy notice aligned itself inside a full-width container, and the container now anchors itself. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../_inc/client/components/global-notices/style.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss index d24e7cb3c41c..209aebd0d6a3 100644 --- a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss +++ b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss @@ -3,7 +3,6 @@ @use "../../scss/calypso-mixins"; .global-notices { - text-align: right; pointer-events: none; z-index: z-index.z-index("root", ".global-notices"); @@ -43,11 +42,13 @@ * Targets children rather than a notice class: the notices are now * `@wordpress/ui` `Notice.Root`, whose CSS-module class name is hashed. * The container is click-through so it never blocks the page, so each notice - * has to opt back in, and undo the right-alignment that stacks them. + * has to opt back in. `container-type` is off because the container shrinks to + * fit its contents, and a size-contained child reports no width — collapsing + * the toast. The query it disables only applies to notices with a title. */ .global-notices > * { + container-type: normal; margin-bottom: 0; - text-align: left; pointer-events: auto; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2), From 946f4a2abbb6bcf13fb9ebd8d39f2e2ba20d2bc2 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 7 Sep 2026 16:14:11 -0300 Subject: [PATCH 06/14] Jetpack notices: draw the toast shadow at the design system's elevation The toast carried a Calypso-era shadow: two layers at alpha 0.2 and 0.15, the second a 56px blur at zero offset. Under a light tinted card that halo reads as a smudge rather than as elevation. `@wordpress/ui` has no shadow token. Its components define a private `--_wp-ui-elevation-*` locally instead, and a floating transient overlay belongs at the same level as a popover or a menu, so this copies that stack. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../_inc/client/components/global-notices/style.scss | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss index 209aebd0d6a3..cd3db272612c 100644 --- a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss +++ b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss @@ -50,9 +50,14 @@ container-type: normal; margin-bottom: 0; pointer-events: auto; + + /* `@wordpress/ui`'s medium elevation, the level its popovers and menus use. + Copied because the design system has no shadow token to reference yet. */ box-shadow: - 0 2px 5px rgba(0, 0, 0, 0.2), - 0 0 56px rgba(0, 0, 0, 0.15); + 0 2px 3px rgba(0, 0, 0, 0.05), + 0 4px 5px rgba(0, 0, 0, 0.04), + 0 12px 12px rgba(0, 0, 0, 0.03), + 0 16px 16px rgba(0, 0, 0, 0.02); @include calypso-mixins.breakpoint( ">660px" ) { margin-bottom: 24px; From 8bf86162a404e5f17418161adb0df07434a703d3 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 7 Sep 2026 17:35:32 -0300 Subject: [PATCH 07/14] Jetpack notices: stop notices announcing themselves, and pad the two remaining flush ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found four things the migration missed. `Notice.Root` defaults `spokenMessage` to its children, so every notice now called `speak()` on mount — including permanent ones like the SEO banners and the block-theme notice, which announced on page load, and the ones inside the `aria-live` stack, which announced twice. The legacy notice never announced, and the notices that should are already inside a live region, so this passes `spokenMessage={ null }`. Two more notices are direct `SettingsCard` children and so ran to the card's edges: the SEO opt-in banner and the reader's WordPress.com notice. Both get the same padded wrapper as account protection. The Sharing buttons block-theme notice moves out of its `SettingsGroup` for the same reason the Like buttons one did: the group paints an opaque scrim over its children in offline mode, which takes pointer events. Three test assertions were reading the old chassis: two on the `dops-notice` class, one on a link's accessible name, which now carries the design system's "(opens in a new tab)" suffix. None of these suites run in CI — `jest.config.gui.js` matches `test/component.js`, not `.jsx`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../components/dash-item/test/component.jsx | 10 ++++----- .../jetpack-notices/test/component.jsx | 3 ++- .../_inc/client/components/notice/index.jsx | 3 +++ .../jetpack/_inc/client/reader/index.jsx | 2 +- .../_inc/client/sharing/share-buttons.jsx | 21 +++++++++---------- .../jetpack/_inc/client/traffic/seo.jsx | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/dash-item/test/component.jsx b/projects/plugins/jetpack/_inc/client/components/dash-item/test/component.jsx index e024e7787d2f..6fbb99b9fcda 100644 --- a/projects/plugins/jetpack/_inc/client/components/dash-item/test/component.jsx +++ b/projects/plugins/jetpack/_inc/client/components/dash-item/test/component.jsx @@ -186,19 +186,17 @@ describe( 'DashItem', () => { }; it( "shows a warning badge when status is 'is-warning'", () => { - const { container } = render( , { + render( , { initialState: buildInitialState(), } ); - // eslint-disable-next-line testing-library/no-container - expect( container.querySelector( '.dops-notice.is-warning' ) ).toBeInTheDocument(); + expect( screen.getByText( 'Updates needed' ) ).toBeInTheDocument(); } ); it( 'when it is activated, the warning badge is linked to Plugins screen in WordPress.com', () => { - const { container } = render( , { + render( , { initialState: buildInitialState(), } ); - // eslint-disable-next-line testing-library/no-container - const node = container.querySelector( '.dops-notice.is-warning' ).closest( 'a' ); + const node = screen.getByText( 'Updates needed' ).closest( 'a' ); expect( node ).toBeInTheDocument(); expect( node ).toHaveAttribute( 'href', diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/test/component.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/test/component.jsx index 7183536a829a..bd27b2ed7d76 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/test/component.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/test/component.jsx @@ -148,6 +148,7 @@ describe( 'DevVersionNotice', () => { expect( screen.getByText( 'You are currently running a development version of Jetpack.' ) ).toBeInTheDocument(); - expect( screen.getByRole( 'link', { name: 'Submit Beta feedback' } ) ).toBeInTheDocument(); + // The accessible name carries the design system's "(opens in a new tab)" suffix. + expect( screen.getByRole( 'link', { name: /Submit Beta feedback/ } ) ).toBeInTheDocument(); } ); } ); diff --git a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx index 79efada0eb5e..e3aa972d4553 100644 --- a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx @@ -102,6 +102,9 @@ export default class SimpleNotice extends Component { // Callers pass either a Gridicon name or an element; only elements work // here, and the intent already picks a sensible default icon. icon={ isValidElement( icon ) ? icon : undefined } + // The legacy notice never announced. Several of these are permanent, and + // the ones that should announce already sit in an aria-live container. + spokenMessage={ null } > { title ? { title } : null } { body } diff --git a/projects/plugins/jetpack/_inc/client/reader/index.jsx b/projects/plugins/jetpack/_inc/client/reader/index.jsx index 42031139d060..ebafa0502c3a 100644 --- a/projects/plugins/jetpack/_inc/client/reader/index.jsx +++ b/projects/plugins/jetpack/_inc/client/reader/index.jsx @@ -155,7 +155,7 @@ function Reader( props ) { hideButton module={ moduleName } > - { woaNotice } + { woaNotice &&
{ woaNotice }
} { readerToggle } { visitReaderLink } diff --git a/projects/plugins/jetpack/_inc/client/sharing/share-buttons.jsx b/projects/plugins/jetpack/_inc/client/sharing/share-buttons.jsx index d3e5bc5c8230..e0b840afcb8f 100644 --- a/projects/plugins/jetpack/_inc/client/sharing/share-buttons.jsx +++ b/projects/plugins/jetpack/_inc/client/sharing/share-buttons.jsx @@ -115,17 +115,7 @@ export const ShareButtons = withModuleSettingsFormHelpers( ); if ( ! shouldUseSharingBlockAction ) { - return ( - <> - { toggle } - { shouldShowSharingBlock && ( - - ) } - - ); + return toggle; } if ( isForcedActive ) { @@ -175,6 +165,15 @@ export const ShareButtons = withModuleSettingsFormHelpers( { moduleAction() } + { ! shouldUseSharingBlockAction && shouldShowSharingBlock && ( +
+ +
+ ) } + { ( isActive || shouldShowSharingBlock ) && ! shouldUseSharingBlockAction && configCard() } diff --git a/projects/plugins/jetpack/_inc/client/traffic/seo.jsx b/projects/plugins/jetpack/_inc/client/traffic/seo.jsx index 6b9d7f54a8dd..570f4086ad6b 100644 --- a/projects/plugins/jetpack/_inc/client/traffic/seo.jsx +++ b/projects/plugins/jetpack/_inc/client/traffic/seo.jsx @@ -290,7 +290,7 @@ export const SEO = withModuleSettingsFormHelpers( saveDisabled={ this.props.isSavingAnyOption( this.constants.moduleOptionsArray ) } hideButton={ hasConflictingSeoPlugin || ! hasSeoTools } > - { this.seoOptInBanner() } +
{ this.seoOptInBanner() }
{ hasSeoTools && ( Date: Mon, 7 Sep 2026 17:40:34 -0300 Subject: [PATCH 08/14] Jetpack notices: lift the conflicting-SEO-plugin notice out of the scrim too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sweep for the same shape found one more: the "Your SEO settings are managed by the following plugin" notice sits inside a `SettingsGroup disableInOfflineMode`, so an offline site with Yoast or Rank Math installed renders it under the scrim. Same treatment as the Like buttons and Sharing buttons notices — outside the group, in the card's padding. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../jetpack/_inc/client/traffic/seo.jsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/traffic/seo.jsx b/projects/plugins/jetpack/_inc/client/traffic/seo.jsx index 570f4086ad6b..7f1abe8569ce 100644 --- a/projects/plugins/jetpack/_inc/client/traffic/seo.jsx +++ b/projects/plugins/jetpack/_inc/client/traffic/seo.jsx @@ -291,6 +291,17 @@ export const SEO = withModuleSettingsFormHelpers( hideButton={ hasConflictingSeoPlugin || ! hasSeoTools } >
{ this.seoOptInBanner() }
+ { hasSeoTools && hasConflictingSeoPlugin && ( +
+ + { sprintf( + /* translators: %s is the name of conflicting SEO plugin */ + __( 'Your SEO settings are managed by the following plugin: %s', 'jetpack' ), + conflictingSeoPlugins[ 0 ].name + ) } + +
+ ) } { hasSeoTools && ( - { hasConflictingSeoPlugin && ( - - { sprintf( - /* translators: %s is the name of conflicting SEO plugin */ - __( 'Your SEO settings are managed by the following plugin: %s', 'jetpack' ), - conflictingSeoPlugins[ 0 ].name - ) } - - ) }

{ __( 'Take control of the way search engines represent your site. With Jetpack’s SEO tools you can preview how your content will look on popular search engines and change items like your site name and tagline in seconds.', From 5ba5bc9b6478a8aeb1068678497e4ca771def933 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 7 Sep 2026 17:52:06 -0300 Subject: [PATCH 09/14] Jetpack notices: don't render the SEO banner's padded wrapper when there is no banner `seoOptInBanner()` returns null unless the SEO package reports the opt-in is available, which is most sites. The padded wrapper around it was unguarded, so those sites got 32px of empty space above the card's description. The call is hoisted so the guard does not run it twice. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- projects/plugins/jetpack/_inc/client/traffic/seo.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/_inc/client/traffic/seo.jsx b/projects/plugins/jetpack/_inc/client/traffic/seo.jsx index 7f1abe8569ce..bb48fd3a5e3d 100644 --- a/projects/plugins/jetpack/_inc/client/traffic/seo.jsx +++ b/projects/plugins/jetpack/_inc/client/traffic/seo.jsx @@ -269,6 +269,7 @@ export const SEO = withModuleSettingsFormHelpers( return acc; }, [] ); const hasConflictingSeoPlugin = conflictingSeoPlugins.length > 0; + const optInBanner = this.seoOptInBanner(); const frontPageMetaCharCountClasses = clsx( { 'jp-seo-front-page-description-count': true, @@ -290,7 +291,7 @@ export const SEO = withModuleSettingsFormHelpers( saveDisabled={ this.props.isSavingAnyOption( this.constants.moduleOptionsArray ) } hideButton={ hasConflictingSeoPlugin || ! hasSeoTools } > -

{ this.seoOptInBanner() }
+ { optInBanner &&
{ optInBanner }
} { hasSeoTools && hasConflictingSeoPlugin && (
From 422ce4e6a885eabf7358101af22adf6876cce707 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 7 Sep 2026 18:48:30 -0300 Subject: [PATCH 10/14] Jetpack notices: drop the broken-link icon from the connection errors Four connection-error notices passed `icon="link-break"`. `Notice.Root` takes an icon element rather than a Gridicon name, so the design system's error icon has been rendering there since the migration and the prop did nothing. Standardising on the intent's icon is the decision, so the props go rather than being converted to elements. `SimpleNotice` still honours an icon passed as an element, which the SocialLogo in `jetpack-notices/index.jsx` relies on. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../jetpack-notices/error-notice-cycle-connection.jsx | 1 - .../components/jetpack-notices/jetpack-connection-errors.jsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/error-notice-cycle-connection.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/error-notice-cycle-connection.jsx index 43addd4dc9c4..25afe5e57550 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/error-notice-cycle-connection.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/error-notice-cycle-connection.jsx @@ -24,7 +24,6 @@ export default class ErrorNoticeCycleConnection extends Component { showDismiss={ false } text={ this.props.text } status={ 'is-error' } - icon={ 'link-break' } display={ this.props.display } > @@ -50,7 +49,6 @@ export default class JetpackConnectionErrors extends Component { @@ -81,7 +79,6 @@ export default class JetpackConnectionErrors extends Component { From 4e85039f60e138d20c6af4bb86ea4260606b89c6 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Tue, 8 Sep 2026 09:22:52 -0300 Subject: [PATCH 11/14] Jetpack notices: keep `display` hiding the notice rather than unmounting it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original swap turned `display={ false }` from "render, then hide with `is-hidden`" into an early `return null`. That unmounts the subtree, and `JetpackNotices` passes `display={ ! isReconnectingSite }` down to the connection errors — whose `NoticeActionReconnect` records `jetpack_termination_error_notice_view` from a mount effect. A failed reconnect therefore unmounted and remounted the notice and recorded the view twice, on top of discarding `useRestoreConnection`'s own state mid-flight. The class comes back, with the rule unlayered because `Notice.Root` sets `display: grid` inside `@layer wp-ui`, which `[hidden]` cannot override. Also clears the props that survived the migration without a job. Nothing passes `icon` to `SimpleNotice` any more, and `isValidElement` silently dropped a string anyway; `NoticeAction`'s `icon` and `variant` never reached the rendered action. A stale value should fail in review rather than do nothing quietly, so the declarations and their last two call sites go together. Whether a secondary action needs its own treatment is still open, and is recorded on the PR. `JetpackConnectionErrors` returns null when nothing renders, which is the empty flex child that `JetpackStateNotices` and `DismissableNotices` were already fixed for, and `Notice.Description` renders a div, since several call sites hand it block content and it defaults to a span. The wrapper now has tests. It had none, which is why the `display` regression survived nine commits: both new display cases fail against the old behaviour. They run once the component suites reach CI in #52056. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- .../jetpack-connection-errors.jsx | 6 +- .../notice-action-reconnect.jsx | 7 +- .../_inc/client/components/notice/index.jsx | 22 +-- .../components/notice/notice-action.jsx | 2 - .../components/notice/test/component.jsx | 137 ++++++++++++++++++ .../_inc/client/scss/shared/_main.scss | 6 + 6 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 projects/plugins/jetpack/_inc/client/components/notice/test/component.jsx diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/jetpack-connection-errors.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/jetpack-connection-errors.jsx index dd969391898f..ee967f710f74 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/jetpack-connection-errors.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/jetpack-connection-errors.jsx @@ -65,11 +65,7 @@ export default class JetpackConnectionErrors extends Component { // Add secondary action if available if ( errorData.secondary_action_url && errorData.secondary_action_label ) { actions.push( - + { errorData.secondary_action_label } ); diff --git a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/notice-action-reconnect.jsx b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/notice-action-reconnect.jsx index 1460babc3077..cd276d3392ed 100644 --- a/projects/plugins/jetpack/_inc/client/components/jetpack-notices/notice-action-reconnect.jsx +++ b/projects/plugins/jetpack/_inc/client/components/jetpack-notices/notice-action-reconnect.jsx @@ -72,15 +72,10 @@ const NoticeActionReconnect = props => { analytics.tracks.recordEvent( 'jetpack_termination_error_notice_view', getEventProps() ); }, [ getEventProps ] ); - return ( - - { props.children } - - ); + return { props.children }; }; NoticeActionReconnect.propTypes = { - icon: PropTypes.string, errorCode: PropTypes.string, errorData: PropTypes.object, action: PropTypes.string, diff --git a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx index e3aa972d4553..903523079160 100644 --- a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx @@ -1,7 +1,7 @@ import { Notice } from '@wordpress/ui'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { Component, isValidElement } from 'react'; +import { Component } from 'react'; const noop = () => {}; @@ -28,7 +28,6 @@ export default class SimpleNotice extends Component { PropTypes.arrayOf( PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ) ), ] ), title: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), - icon: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), onDismissClick: PropTypes.func, className: PropTypes.string, display: PropTypes.bool, @@ -74,7 +73,6 @@ export default class SimpleNotice extends Component { const { children, className, - icon, isCompact, onDismissClick, showDismiss = ! isCompact, // by default, show on normal notices, don't show on compact ones @@ -84,10 +82,6 @@ export default class SimpleNotice extends Component { display, } = this.props; - if ( ! display ) { - return null; - } - // `text` marks the caller as using the two-slot form, where children are the // actions. Without it, children are the body. const body = text ? this.clearText( text ) : children; @@ -96,18 +90,18 @@ export default class SimpleNotice extends Component { return ( { title ? { title } : null } - { body } + { ( body || body === 0 ) && ( + }>{ body } + ) } { actions ? { actions } : null } { showDismiss && } diff --git a/projects/plugins/jetpack/_inc/client/components/notice/notice-action.jsx b/projects/plugins/jetpack/_inc/client/components/notice/notice-action.jsx index 51b121863ced..c597f729bb0a 100644 --- a/projects/plugins/jetpack/_inc/client/components/notice/notice-action.jsx +++ b/projects/plugins/jetpack/_inc/client/components/notice/notice-action.jsx @@ -9,8 +9,6 @@ export default class NoticeAction extends Component { href: PropTypes.string, onClick: PropTypes.func, external: PropTypes.bool, - icon: PropTypes.string, - variant: PropTypes.oneOf( [ 'primary', 'secondary' ] ), }; static defaultProps = { diff --git a/projects/plugins/jetpack/_inc/client/components/notice/test/component.jsx b/projects/plugins/jetpack/_inc/client/components/notice/test/component.jsx new file mode 100644 index 000000000000..810e2d1b3571 --- /dev/null +++ b/projects/plugins/jetpack/_inc/client/components/notice/test/component.jsx @@ -0,0 +1,137 @@ +import { render, screen } from 'test/test-utils'; +import SimpleNotice from '../index'; +import NoticeAction from '../notice-action'; + +describe( 'SimpleNotice', () => { + it( 'renders its children as the body when there is no text', () => { + render( Body copy ); + expect( screen.getByText( 'Body copy' ) ).toBeInTheDocument(); + } ); + + it( 'renders children as actions when text is set', () => { + render( + + Do the thing + + ); + expect( screen.getByText( 'The message' ) ).toBeInTheDocument(); + expect( screen.getByRole( 'link', { name: /Do the thing/ } ) ).toHaveAttribute( + 'href', + 'https://example.com/go' + ); + } ); + + it( 'renders a title above the body', () => { + render( + +
The body
+
+ ); + expect( screen.getByText( 'The heading' ) ).toBeInTheDocument(); + expect( screen.getByText( 'The body' ) ).toBeInTheDocument(); + } ); + + it.each( [ + [ 'is-error', 'error' ], + [ 'is-warning', 'warning' ], + [ 'is-success', 'success' ], + [ 'is-info', 'info' ], + ] )( 'maps status %s to the %s intent', ( status, intent ) => { + const { container } = render( Text ); + // eslint-disable-next-line testing-library/no-container + expect( container.querySelector( '.jp-notice' ).className ).toMatch( + new RegExp( `is-${ intent }` ) + ); + } ); + + // Regression: `display` used to unmount the subtree. Children such as + // NoticeActionReconnect record a Tracks event on mount, so it must hide instead. + it( 'keeps the notice mounted but hidden when display is false', () => { + const onMount = jest.fn(); + const Child = () => { + onMount(); + return Still here; + }; + + const { container } = render( + + + + ); + + // eslint-disable-next-line testing-library/no-container + expect( container.querySelector( '.jp-notice' ) ).toHaveClass( 'is-hidden' ); + expect( onMount ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'does not re-mount its children when display flips', () => { + const onMount = jest.fn(); + const Child = () => { + onMount(); + return Child; + }; + + const { rerender } = render( + + + + ); + onMount.mockClear(); + + rerender( + + + + ); + rerender( + + + + ); + + // Re-renders, but never a fresh mount. + expect( screen.getByText( 'Child' ) ).toBeInTheDocument(); + expect( onMount ).toHaveBeenCalledTimes( 2 ); + } ); + + it( 'calls onDismissClick from the close button', async () => { + const onDismissClick = jest.fn(); + render( + + ); + screen.getByRole( 'button', { name: 'Dismiss' } ).click(); + expect( onDismissClick ).toHaveBeenCalled(); + } ); + + it( 'renders no close button when showDismiss is false', () => { + render( ); + expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument(); + } ); +} ); + +describe( 'NoticeAction', () => { + it( 'renders a button when it has no href', () => { + const onClick = jest.fn(); + render( + + Press me + + ); + screen.getByRole( 'button', { name: 'Press me' } ).click(); + expect( onClick ).toHaveBeenCalled(); + } ); + + it( 'opens external links in a new tab', () => { + render( + + + Read the docs + + + ); + expect( screen.getByRole( 'link', { name: /Read the docs/ } ) ).toHaveAttribute( + 'target', + '_blank' + ); + } ); +} ); diff --git a/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss b/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss index 06f57b79c439..463d6f1b502e 100644 --- a/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss +++ b/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss @@ -55,6 +55,12 @@ text-decoration-thickness: from-font; } +// Unlayered on purpose: `Notice.Root` sets `display: grid` +// inside `@layer wp-ui`, which `[hidden]` cannot override. +.jp-notice.is-hidden { + display: none; +} + // restyled the arrow to match our gray .toplevel_page_jetpack ul#adminmenu a.wp-has-current-submenu::after { border-right-color: colors.$gray-light; From ace63c77f3d64247779cd63cad7af48f213b29c1 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Tue, 8 Sep 2026 17:08:35 -0300 Subject: [PATCH 12/14] Jetpack notices: correct what happens to the underline rule upstream The comment said to drop this rule once the design system grows a `text-decoration` bridge. That would regress three notices: the upstream defense only reaches elements carrying wp-ui's `Link` class, and the offline-mode notice and two state notices interpolate a raw `` that nothing else underlines. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA --- projects/plugins/jetpack/_inc/client/scss/shared/_main.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss b/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss index 463d6f1b502e..59cf1a11900f 100644 --- a/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss +++ b/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss @@ -49,7 +49,10 @@ // `@wordpress/ui` styles all sit inside `@layer wp-ui`, which loses to the // unlayered rule above, and its `Link` relies on the UA underline rather than // setting one. The shorthand there also resets thickness, hence both lines. -// Drop this once `--_gcd-a-text-decoration-line` lands upstream. +// Once WordPress/gutenberg#82620 ships and we take the bump, this +// narrows rather than goes: the upstream defense only reaches wp-ui +// `Link`, and three notices interpolate a raw `` that would lose +// its underline. .jetpack-pagestyles .jp-notice a { text-decoration-line: underline; text-decoration-thickness: from-font; From 941ede6ae0fdf4c3c5fe1fa1172124f7bb78caf0 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 9 Sep 2026 10:46:05 -0300 Subject: [PATCH 13/14] Jetpack notices: trim the review nits before merge Four small items left from review, none behavioural: - The changelog entry capitalises after the component prefix, per AGENTS.md. - Two comment blocks come back inside the budget. The `.global-notices > *` block carried three unrelated rationales in one six-line essay; each trap now sits on the declaration it explains. The underline rule's seven-line note loses the upstream detail that duplicates the linked issue. - `dismissText` is read in `render()`, so it is declared in `propTypes`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SWVs5oUDimen1FeGJhJAkx --- .../client/components/global-notices/style.scss | 16 +++++++--------- .../_inc/client/components/notice/index.jsx | 1 + .../jetpack/_inc/client/scss/shared/_main.scss | 13 +++++-------- .../changelog/update-simple-notice-wpds-wrapper | 2 +- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss index cd3db272612c..28e575707086 100644 --- a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss +++ b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss @@ -38,17 +38,15 @@ } } -/* - * Targets children rather than a notice class: the notices are now - * `@wordpress/ui` `Notice.Root`, whose CSS-module class name is hashed. - * The container is click-through so it never blocks the page, so each notice - * has to opt back in. `container-type` is off because the container shrinks to - * fit its contents, and a size-contained child reports no width — collapsing - * the toast. The query it disables only applies to notices with a title. - */ +/* Targets children: `Notice.Root`'s own class is a hashed CSS-module name. */ .global-notices > * { - container-type: normal; margin-bottom: 0; + + /* A size-contained child reports no width in this shrink-to-fit + container, collapsing the toast. Only titled notices query it. */ + container-type: normal; + + /* The container is click-through, so each notice opts back in. */ pointer-events: auto; /* `@wordpress/ui`'s medium elevation, the level its popovers and menus use. diff --git a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx index 903523079160..c51f47adf528 100644 --- a/projects/plugins/jetpack/_inc/client/components/notice/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/notice/index.jsx @@ -29,6 +29,7 @@ export default class SimpleNotice extends Component { ] ), title: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), onDismissClick: PropTypes.func, + dismissText: PropTypes.string, className: PropTypes.string, display: PropTypes.bool, }; diff --git a/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss b/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss index 59cf1a11900f..0e73b8c6cea0 100644 --- a/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss +++ b/projects/plugins/jetpack/_inc/client/scss/shared/_main.scss @@ -46,16 +46,13 @@ text-decoration: underline; } -// `@wordpress/ui` styles all sit inside `@layer wp-ui`, which loses to the -// unlayered rule above, and its `Link` relies on the UA underline rather than -// setting one. The shorthand there also resets thickness, hence both lines. -// Once WordPress/gutenberg#82620 ships and we take the bump, this -// narrows rather than goes: the upstream defense only reaches wp-ui -// `Link`, and three notices interpolate a raw `` that would lose -// its underline. +// `@wordpress/ui` sits in `@layer wp-ui`, which loses to the unlayered +// `.jetpack-pagestyles a` above, and its `Link` relies on the UA underline. +// WordPress/gutenberg#82620 narrows this rather than retiring it: the upstream +// defense reaches wp-ui `Link` only, and three notices interpolate a raw ``. .jetpack-pagestyles .jp-notice a { text-decoration-line: underline; - text-decoration-thickness: from-font; + text-decoration-thickness: from-font; // `text-decoration: none` above resets it. } // Unlayered on purpose: `Notice.Root` sets `display: grid` diff --git a/projects/plugins/jetpack/changelog/update-simple-notice-wpds-wrapper b/projects/plugins/jetpack/changelog/update-simple-notice-wpds-wrapper index b6f9ee70f6e4..dc353b49b39e 100644 --- a/projects/plugins/jetpack/changelog/update-simple-notice-wpds-wrapper +++ b/projects/plugins/jetpack/changelog/update-simple-notice-wpds-wrapper @@ -1,4 +1,4 @@ Significance: minor Type: enhancement -Notices: render Jetpack admin notices with the WordPress design system Notice component. +Notices: Render Jetpack admin notices with the WordPress design system Notice component. From 1984c63cd6e4c110103df50050267093307a1da6 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 9 Sep 2026 11:12:33 -0300 Subject: [PATCH 14/14] Jetpack notices: say why the toast elevation is copied `--_wp-ui-elevation-md` is not a token: `@wordpress/ui` redeclares it on each component's own element (popover's `.surface`, item-popup's `.popup`), inside `@layer wp-ui`, and never publishes it. Referencing it here would resolve to nothing and drop the shadow silently. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SWVs5oUDimen1FeGJhJAkx --- .../jetpack/_inc/client/components/global-notices/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss index 28e575707086..3d7a3ecde72c 100644 --- a/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss +++ b/projects/plugins/jetpack/_inc/client/components/global-notices/style.scss @@ -49,8 +49,8 @@ /* The container is click-through, so each notice opts back in. */ pointer-events: auto; - /* `@wordpress/ui`'s medium elevation, the level its popovers and menus use. - Copied because the design system has no shadow token to reference yet. */ + /* `@wordpress/ui`'s medium elevation. Copied, not referenced: its + `--_wp-ui-elevation-md` is redeclared per component, never published. */ box-shadow: 0 2px 3px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.04),