diff --git a/projects/js-packages/connection/changelog/update-connection-js-package-ui-pt5 b/projects/js-packages/connection/changelog/update-connection-js-package-ui-pt5
new file mode 100644
index 000000000000..0af4b5cca6ff
--- /dev/null
+++ b/projects/js-packages/connection/changelog/update-connection-js-package-ui-pt5
@@ -0,0 +1,4 @@
+Significance: patch
+Type: changed
+
+Connection UI: Migrate the disconnect, manage-connection and owner-disconnect modals from the @wordpress/components Modal to the @wordpress/ui Dialog.
diff --git a/projects/js-packages/connection/components/disconnect-dialog/index.tsx b/projects/js-packages/connection/components/disconnect-dialog/index.tsx
index a05729b8b791..98a11ffce179 100644
--- a/projects/js-packages/connection/components/disconnect-dialog/index.tsx
+++ b/projects/js-packages/connection/components/disconnect-dialog/index.tsx
@@ -1,10 +1,10 @@
import jetpackAnalytics from '@automattic/jetpack-analytics';
import restApi from '@automattic/jetpack-api';
import { jetpackConfigHas, jetpackConfigGet } from '@automattic/jetpack-config';
-import { Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useMemo, useEffect, useCallback, useState } from 'react';
import useRestApiInit from '../../hooks/use-rest-api-init';
+import ConnectionDialog from '../shared/connection-dialog';
import './style.scss';
import StepDisconnect from './steps/step-disconnect';
import StepDisconnectConfirm from './steps/step-disconnect-confirm';
@@ -270,7 +270,6 @@ const DisconnectDialog = ( {
const handleDisconnect = useCallback(
( e?: MouseEvent< HTMLElement > ) => {
e && e.preventDefault();
-
setDisconnectError( false );
setIsDisconnecting( true );
@@ -425,27 +424,17 @@ const DisconnectDialog = ( {
};
return (
- <>
- { isOpen && (
-
{ createInterpolateElement( __( diff --git a/projects/js-packages/connection/components/disconnect-dialog/style.scss b/projects/js-packages/connection/components/disconnect-dialog/style.scss index d444bf40f5ec..93ea19255198 100644 --- a/projects/js-packages/connection/components/disconnect-dialog/style.scss +++ b/projects/js-packages/connection/components/disconnect-dialog/style.scss @@ -1,15 +1,28 @@ @use "@automattic/jetpack-base-styles/style"; .jp-connection__disconnect-dialog { + --viewport-inset: 3rem; + + display: flex; + flex-direction: column; + padding: 0; + overflow-y: auto; h1 { - margin-top: 0; + --_gcd-heading-font-size: var(--font-title-small); + --_gcd-heading-font-weight: 600; + --_gcd-heading-margin: 0 0 0.67em; + + margin: 0 0 0.67em; line-height: 1.2; font-size: var(--font-title-small); font-weight: 600; } h2 { + --_gcd-heading-font-size: var(--font-title-small); + --_gcd-heading-font-weight: 400; + margin: 0; line-height: 1.2; font-size: var(--font-title-small); @@ -17,13 +30,26 @@ } p { + --_gcd-p-font-size: var(--font-body); + margin-top: 0; font-size: var(--font-body); } + // `Text`'s own stylesheet sets `margin: 0` (layered), which for a `
`
+ // falls back to that default anyway, but for a bare `Text` (renders as a
+ // ``, e.g. `OwnerDisconnectDialog`'s copy) leaves no gap before the
+ // next element, since a `` has no UA margin of its own to restore.
+ // Same story for `line-height`: wp-admin's global `common.css` sets a bare
+ // `p { line-height: 1.5 }` that the raw ` ` steps pick up for free, but a
+ // `` never matches that selector, so it's restored explicitly here.
&__large-text,
p.jp-connection__disconnect-dialog__large-text {
+ --_gcd-p-font-size: 1.25rem;
+
+ margin: 0 0 1em;
font-size: 1.25rem;
+ line-height: 1.5;
display: block;
margin-block: 0 1em;
}
@@ -56,25 +82,6 @@
border-radius: 4px;
}
- .components-modal {
-
- &__content {
- padding: 0;
- display: flex;
- flex-direction: column;
- flex-grow: 1;
- margin: 0;
-
- &::before {
- display: none;
- }
- }
-
- &__header {
- display: none;
- }
- }
-
// The help message grows to fill the row; the button pair keeps its natural
// width beside it and wraps below once the row runs out of room. The
// flex-basis biases toward wrapping early, while `min-width: 0` lets the
@@ -117,6 +124,23 @@
background: var(--jp-red) !important;
}
+ // Moved here from the owner dialog's stylesheet, which no longer renders
+ // these buttons — `DisconnectActionFooter` supplies its own footer actions.
+ // The @wordpress/ui Button emits no `.components-button` class, so the
+ // shared reset that wp-admin would otherwise apply is restored on the
+ // class itself.
+ &__btn-dismiss,
+ &__btn-disconnect {
+ border: 0;
+ border-radius: 4px;
+ color: var(--wp-components-color-accent-inverted, #fff);
+ font-size: var(--font-body-small);
+ height: 40px;
+ text-decoration: none;
+ text-shadow: none;
+ white-space: nowrap;
+ }
+
&__btn-back-to-wp {
background: var(--jp-black) !important;
}
@@ -189,8 +213,7 @@
@media (min-width: 600px) {
- .jp-connection__disconnect-dialog,
- .jp-connection__disconnect-dialog.components-modal__frame {
+ .jp-connection__disconnect-dialog {
width: 100%;
max-width: calc(100% - 32px);
}
@@ -210,8 +233,7 @@
@media (min-width: 960px) {
- .jp-connection__disconnect-dialog,
- .jp-connection__disconnect-dialog.components-modal__frame {
+ .jp-connection__disconnect-dialog {
width: 1200px;
height: 900px;
display: flex;
@@ -221,11 +243,15 @@
.jp-connection__disconnect-dialog {
h1 {
+ --_gcd-heading-font-size: var(--font-title-large);
+
font-size: var(--font-title-large);
}
&__large-text,
p.jp-connection__disconnect-dialog__large-text {
+ --_gcd-p-font-size: 1.5rem;
+
font-size: 1.5rem;
}
diff --git a/projects/js-packages/connection/components/disconnect-dialog/test/component.tsx b/projects/js-packages/connection/components/disconnect-dialog/test/component.tsx
index bce8066f20d8..4fc24981e108 100644
--- a/projects/js-packages/connection/components/disconnect-dialog/test/component.tsx
+++ b/projects/js-packages/connection/components/disconnect-dialog/test/component.tsx
@@ -38,7 +38,7 @@ describe( 'DisconnectDialog', () => {
} );
describe( 'Initially', () => {
- it( 'renders the Modal', () => {
+ it( 'renders the Dialog', () => {
render(
- { __(
- 'Disconnecting the owner account will remove the Jetpack connection for all users on this site. The site will remain connected.',
- 'jetpack-connection-js'
- ) }
-
+
{ __(
'Disconnecting the owner account will remove the Jetpack connection for all users on this site. The site will remain connected.',
'jetpack'
@@ -123,7 +123,7 @@ const OwnerDisconnectDialog = ( { isOpen, onClose, apiRoot, apiNonce, onDisconne
action="check-users"
/>
{ disconnectError } { disconnectError }.
expect(
within(
screen.getByRole( 'dialog', { name: 'Are you sure you want to disconnect?' } )
- ).getByRole( 'heading' )
+ ).getByRole( 'heading', { level: 1 } )
).toHaveTextContent( 'Are you sure you want to disconnect?' );
} );
} );
diff --git a/projects/js-packages/connection/components/disconnect-dialog/test/step-disconnect-confirm.tsx b/projects/js-packages/connection/components/disconnect-dialog/test/step-disconnect-confirm.tsx
index 3d33161bd176..ab2819914c02 100644
--- a/projects/js-packages/connection/components/disconnect-dialog/test/step-disconnect-confirm.tsx
+++ b/projects/js-packages/connection/components/disconnect-dialog/test/step-disconnect-confirm.tsx
@@ -1,7 +1,17 @@
import { jest } from '@jest/globals';
-import { render, screen } from '@testing-library/react';
+import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import { Dialog } from '@wordpress/ui';
import StepDisconnectConfirm from '../steps/step-disconnect-confirm';
+import type { ReactElement, ReactNode } from 'react';
+
+// The step renders its heading as a `Dialog.Title`, which needs a Dialog
+// context. In the app that's always `ConnectionDialog`; in isolation, supply a
+// minimal one so the heading can register.
+const DialogWrapper = ( { children }: { children: ReactNode } ) => (
+
{ title }
-
- { __( 'Disconnect Owner Account', 'jetpack-connection-js' ) }
-
- doubles as the accessible name.
expect( screen.getByRole( 'dialog', { name: dialogName } ) ).toBeInTheDocument();
expect(
- within( screen.getByRole( 'dialog', { name: dialogName } ) ).getByRole( 'heading' )
+ within( screen.getByRole( 'dialog', { name: dialogName } ) ).getByRole( 'heading', {
+ level: 1,
+ } )
).toHaveTextContent( dialogName );
} );
@@ -107,7 +110,9 @@ describe( 'OwnerDisconnectDialog', () => {
await user.click( screen.getByRole( 'button', { name: 'Disconnect' } ) );
await expect(
- screen.findByText( /There was a problem disconnecting your account/ )
+ within( screen.getByRole( 'dialog', { name: dialogName } ) ).findByText(
+ /There was a problem disconnecting your account/
+ )
).resolves.toBeInTheDocument();
// Button re-enabled after failure so the user can retry.
expect( screen.getByRole( 'button', { name: 'Disconnect' } ) ).toBeEnabled();
diff --git a/projects/js-packages/connection/components/shared/connection-dialog/index.tsx b/projects/js-packages/connection/components/shared/connection-dialog/index.tsx
new file mode 100644
index 000000000000..09e447e88883
--- /dev/null
+++ b/projects/js-packages/connection/components/shared/connection-dialog/index.tsx
@@ -0,0 +1,124 @@
+import {
+ Dialog,
+ VisuallyHidden,
+ getWpCompatOverlaySlot,
+ useEnableWpCompatOverlaySlot,
+} from '@wordpress/ui';
+import { useCallback, useRef } from 'react';
+import type { ComponentProps, ReactNode } from 'react';
+
+type DialogOpenChange = NonNullable< ComponentProps< typeof Dialog.Root >[ 'onOpenChange' ] >;
+
+interface ConnectionDialogBaseProps {
+ /** Whether the dialog is open. */
+ isOpen?: boolean;
+ /** Callback fired when the dialog asks to close. */
+ onClose: VoidFunction;
+ /** Whether pressing Escape dismisses the dialog. */
+ dismissOnEscape?: boolean;
+ /** Class name applied to the dialog popup. */
+ className?: string;
+ /** The dialog body. */
+ children?: ReactNode;
+}
+
+/**
+ * The title is set exactly one way, enforced by the type:
+ *
+ * `hasOwnTitle`: the body renders its own `ConnectionDialogTitle` (a
+ * `Dialog.Title`), which supplies the accessible name. `title` is then neither
+ * needed nor allowed.
+ */
+export type ConnectionDialogProps = ConnectionDialogBaseProps &
+ ( { hasOwnTitle: true; title?: never } | { hasOwnTitle?: false; title: string } );
+
+/**
+ * Shared `@wordpress/ui` Dialog scaffolding for the connection dialogs.
+ *
+ * Centralises the dismissal policy the connection dialogs share: they are
+ * modal, cannot be dismissed by clicking the backdrop, and (by default) ignore
+ * Escape, so leaving is an explicit choice made through a footer action.
+ *
+ * @param {ConnectionDialogProps} props - The component props.
+ * @return {import('react').ReactNode} - The ConnectionDialog component.
+ */
+const ConnectionDialog = ( {
+ isOpen,
+ onClose,
+ title,
+ hasOwnTitle = false,
+ dismissOnEscape = false,
+ className,
+ children,
+}: ConnectionDialogProps ) => {
+ // `Dialog` portals to `` with no z-index of its own, which leaves it
+ // below wp-admin chrome such as the admin menu. The compat overlay slot is
+ // the package's own answer to that.
+ useEnableWpCompatOverlaySlot();
+
+ // wp-admin screens are dense with links, and Base UI focuses the first
+ // tabbable element on open — which lands on whatever link happens to come
+ // first in the body. Focus the popup instead, as the old Modal did.
+ const popupRef = useRef< HTMLDivElement >( null );
+
+ const handleOpenChange = useCallback< DialogOpenChange >(
+ ( open, eventDetails ) => {
+ if ( open ) {
+ return;
+ }
+
+ if ( ! dismissOnEscape && eventDetails.reason === 'escape-key' ) {
+ // Leave the dialog open, and let the keydown reach listeners
+ // further up the tree (the disconnect step tracks its own).
+ eventDetails.cancel();
+ eventDetails.allowPropagation();
+ return;
+ }
+
+ onClose();
+ },
+ [ dismissOnEscape, onClose ]
+ );
+
+ return (
+
` to preserve the heading level the connection dialogs
+ * used before the `Modal` migration.
+ *
+ * @param {ConnectionDialogTitleProps} props - The component props.
+ * @return {import('react').ReactNode} - The ConnectionDialogTitle component.
+ */
+export const ConnectionDialogTitle = ( { children, ...props }: ConnectionDialogTitleProps ) => (
+
+
{ __( 'Disconnect Owner Account', 'jetpack' ) }
-