diff --git a/.changelogs/fix-3340-certificate-editor-alignment.yml b/.changelogs/fix-3340-certificate-editor-alignment.yml
new file mode 100644
index 0000000000..02796d26eb
--- /dev/null
+++ b/.changelogs/fix-3340-certificate-editor-alignment.yml
@@ -0,0 +1,5 @@
+significance: patch
+type: fixed
+links:
+ - "#3340"
+entry: Fixed certificate editor sidebar control layout.
diff --git a/assets/scss/admin.scss b/assets/scss/admin.scss
index 47eb2fda40..28c14a3e36 100644
--- a/assets/scss/admin.scss
+++ b/assets/scss/admin.scss
@@ -80,6 +80,7 @@ $llms-buttons-theme-inherit: false;
@import "admin/metaboxes/metabox-students";
@import "admin/metaboxes/metabox-field-repeater";
@import "admin/metaboxes/builder-launcher";
+@import "admin/certificate-editor";
@import "admin/media-protection";
@import "admin/post-tables/llms_orders";
diff --git a/assets/scss/admin/_certificate-editor.scss b/assets/scss/admin/_certificate-editor.scss
new file mode 100644
index 0000000000..37f37ab468
--- /dev/null
+++ b/assets/scss/admin/_certificate-editor.scss
@@ -0,0 +1,52 @@
+/**
+ * Certificate / achievement editor document sidebar.
+ *
+ * WP 7.0+ bumped Gutenberg TextControl height to 40px. The inner-margin
+ * labels used a negative top margin sized for the old shorter inputs, which
+ * then sat on top of the fields. Classic side metaboxes (sync awarded) also
+ * sat flush against the settings panel, and the full-width Sync button
+ * overflowed the ~280px complementary area.
+ */
+
+.llms-certificate-doc-settings.components-panel__body {
+ .components-panel__body-content {
+ padding-bottom: 16px;
+ }
+
+ .llms-certificate-sequential-id-control {
+ margin-bottom: 0;
+ }
+
+ .llms-certificate-margin-control__label {
+ display: block;
+ margin: 4px 0 0 4px;
+ }
+}
+
+.interface-complementary-area #poststuff,
+.editor-sidebar #poststuff,
+.edit-post-sidebar #poststuff {
+ box-sizing: border-box;
+ min-width: 0;
+ width: 100%;
+}
+
+#certificate_sync,
+#achievement_sync {
+ margin-top: 16px;
+
+ .inside {
+ overflow-x: hidden;
+ }
+
+ .llms-sync-action-wrap {
+ margin: 1em 0 0;
+ }
+
+ .sync-action {
+ box-sizing: border-box;
+ display: inline-block;
+ max-width: 100%;
+ width: auto;
+ }
+}
diff --git a/includes/abstracts/llms-abstract-meta-box-user-engagement-sync.php b/includes/abstracts/llms-abstract-meta-box-user-engagement-sync.php
index 64757b831a..2771954cd4 100644
--- a/includes/abstracts/llms-abstract-meta-box-user-engagement-sync.php
+++ b/includes/abstracts/llms-abstract-meta-box-user-engagement-sync.php
@@ -295,8 +295,8 @@ private function sync_action() {
ob_start();
?>
-
-
+
+
+
- { getDesc( index ) }
+
+ { getDesc( index ) }
+
);
}
@@ -104,11 +113,17 @@ export default function MarginsControl( { margins } ) {
return (
-
- { margins.map( ( margin, index ) => (
) ) }
+
+ { margins.map( ( margin, index ) => (
+
+ ) ) }
);
diff --git a/src/js/admin-certificate-editor/plugin/sequential-id-control.js b/src/js/admin-certificate-editor/plugin/sequential-id-control.js
index dd0f92c84a..6091c11e74 100644
--- a/src/js/admin-certificate-editor/plugin/sequential-id-control.js
+++ b/src/js/admin-certificate-editor/plugin/sequential-id-control.js
@@ -24,18 +24,23 @@ export default function SequentialIdControl( { sequentialId } ) {
}
return (
-
{
- setId( val );
- editCertificate( 'sequential_id', val );
- } }
- help={ __( 'Used for the {sequential_id} merge code when generating a certificate from this template.', 'lifterlms' ) }
- />
+
+ {
+ setId( val );
+ editCertificate( 'sequential_id', val );
+ } }
+ help={ __(
+ 'Used for the {sequential_id} merge code when generating a certificate from this template.',
+ 'lifterlms'
+ ) }
+ />
+
);
}
diff --git a/tests/e2e/specs/admin/certificate-editor-layout.spec.js b/tests/e2e/specs/admin/certificate-editor-layout.spec.js
new file mode 100644
index 0000000000..6f1ace19d1
--- /dev/null
+++ b/tests/e2e/specs/admin/certificate-editor-layout.spec.js
@@ -0,0 +1,163 @@
+/**
+ * Certificate editor document sidebar layout (WP 7.0+)
+ *
+ * WP 7.0 raised default Gutenberg TextControl height to 40px. Inner-margin
+ * labels must sit below those inputs, and the Sync awarded metabox must not
+ * overflow the document sidebar.
+ */
+
+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
+
+/**
+ * Create a published certificate template and award it once so the Sync button renders.
+ *
+ * @param {import('@wordpress/e2e-test-utils-playwright').RequestUtils} requestUtils Request utils.
+ * @return {Promise} Template post ID.
+ */
+async function createTemplateWithAward( requestUtils ) {
+ const suffix = Date.now();
+ const template = await requestUtils.rest( {
+ method: 'POST',
+ path: '/llms/v1/certificates',
+ data: {
+ title: `Certificate Layout ${ suffix }`,
+ content: 'Certificate body.
',
+ status: 'publish',
+ },
+ } );
+
+ const me = await requestUtils.rest( {
+ method: 'GET',
+ path: '/wp/v2/users/me',
+ } );
+
+ await requestUtils.rest( {
+ method: 'POST',
+ path: '/llms/v1/awarded-certificates',
+ data: {
+ student_id: me.id,
+ certificate_id: template.id,
+ },
+ } );
+
+ return template.id;
+}
+
+/**
+ * Open the Certificate Template document sidebar and ensure Settings is expanded.
+ *
+ * @param {import('@wordpress/e2e-test-utils-playwright').Editor} editor Editor utils.
+ * @param {import('@playwright/test').Page} page Playwright page.
+ * @return {Promise}
+ */
+async function openCertificateSettings( editor, page ) {
+ await editor.openDocumentSettingsSidebar();
+
+ const documentTab = page.getByRole( 'tab', { name: 'Certificate Template' } );
+ if ( await documentTab.count() ) {
+ await documentTab.click();
+ }
+
+ const settingsPanel = page.locator( '.llms-certificate-doc-settings' );
+ await expect( settingsPanel ).toBeVisible();
+
+ const toggle = settingsPanel.locator( '.components-panel__body-toggle' );
+ if ( await toggle.count() ) {
+ const expanded = await toggle.getAttribute( 'aria-expanded' );
+ if ( expanded === 'false' ) {
+ await toggle.click();
+ }
+ }
+}
+
+test.describe( 'Admin/CertificateEditorLayout', () => {
+
+ test( 'Margin labels sit below inputs and the Sync panel fits the sidebar', async ( { admin, editor, page, requestUtils } ) => {
+ await admin.visitAdminPage( '/' );
+
+ const isWp70 = await page.evaluate( () => document.body.classList.contains( 'llms-wp-version-gte-70' ) );
+ test.skip( ! isWp70, 'WP 7.0+ form-control sizing only' );
+
+ const templateId = await createTemplateWithAward( requestUtils );
+ await admin.editPost( templateId );
+
+ await openCertificateSettings( editor, page );
+
+ const topInput = page.locator( '#llms-certificate-control--margin--top' );
+ await expect( topInput ).toBeVisible();
+
+ const topLabel = page.locator( '.llms-certificate-margin-control' ).filter( {
+ has: page.locator( '#llms-certificate-control--margin--top' ),
+ } ).locator( '.llms-certificate-margin-control__label' );
+
+ await expect( topLabel ).toHaveText( 'Top' );
+
+ const overlap = await page.evaluate( () => {
+ const sides = [ 'top', 'right', 'bottom', 'left' ];
+ return sides.map( ( side ) => {
+ const input = document.querySelector( `#llms-certificate-control--margin--${ side }` );
+ const label = input?.closest( '.llms-certificate-margin-control' )
+ ?.querySelector( '.llms-certificate-margin-control__label' );
+ if ( ! input || ! label ) {
+ return { side, missing: true };
+ }
+ const inputBox = input.getBoundingClientRect();
+ const labelBox = label.getBoundingClientRect();
+ return {
+ side,
+ inputBottom: inputBox.bottom,
+ labelTop: labelBox.top,
+ gap: labelBox.top - inputBox.bottom,
+ inputHeight: inputBox.height,
+ };
+ } );
+ } );
+
+ for ( const side of overlap ) {
+ expect( side.missing, `${ side.side } margin control missing` ).toBeFalsy();
+ expect( side.inputHeight, `${ side.side } input height` ).toBeGreaterThanOrEqual( 36 );
+ expect( side.gap, `${ side.side } label overlaps input` ).toBeGreaterThanOrEqual( 0 );
+ }
+
+ const sequentialHelp = page.locator( '.llms-certificate-sequential-id-control .components-base-control__help' );
+ await expect( sequentialHelp ).toBeVisible();
+
+ const syncBox = page.locator( '#certificate_sync' );
+ await expect( syncBox ).toBeVisible();
+ await syncBox.scrollIntoViewIfNeeded();
+
+ const syncSpacing = await page.evaluate( () => {
+ const help = document.querySelector( '.llms-certificate-sequential-id-control .components-base-control__help' );
+ const heading = document.querySelector( '#certificate_sync .postbox-header, #certificate_sync h2.hndle' );
+ if ( ! help || ! heading ) {
+ return null;
+ }
+ return heading.getBoundingClientRect().top - help.getBoundingClientRect().bottom;
+ } );
+
+ expect( syncSpacing ).not.toBeNull();
+ expect( syncSpacing ).toBeGreaterThanOrEqual( 12 );
+
+ const syncButton = syncBox.locator( 'a.sync-action' );
+ await expect( syncButton ).toBeVisible();
+
+ const sidebarFit = await page.evaluate( () => {
+ const sidebar = document.querySelector( '.interface-complementary-area, .editor-sidebar, .edit-post-sidebar' );
+ const button = document.querySelector( '#certificate_sync a.sync-action' );
+ if ( ! sidebar || ! button ) {
+ return null;
+ }
+ const sidebarBox = sidebar.getBoundingClientRect();
+ const buttonBox = button.getBoundingClientRect();
+ return {
+ scrollOverflow: sidebar.scrollWidth - sidebar.clientWidth,
+ buttonOverflow: buttonBox.right - sidebarBox.right,
+ };
+ } );
+
+ expect( sidebarFit ).not.toBeNull();
+ expect( sidebarFit.scrollOverflow ).toBeLessThanOrEqual( 1 );
+ expect( sidebarFit.buttonOverflow ).toBeLessThanOrEqual( 1 );
+ } );
+
+} );