Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changelogs/fix-3340-certificate-editor-alignment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
significance: patch
type: fixed
links:
- "#3340"
entry: Fixed certificate editor sidebar control layout.
1 change: 1 addition & 0 deletions assets/scss/admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
52 changes: 52 additions & 0 deletions assets/scss/admin/_certificate-editor.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ private function sync_action() {
ob_start();
?>
<p><?php echo wp_kses_post( $texts['sync_description'] ); ?></p>
<p style="text-align: right; margin: 1em 0;">
<a href="<?php echo esc_url( $sync_url ); ?>" class="llms-button-primary sync-action full small" onclick="return confirm('<?php echo esc_js( $sync_alert ); ?>')" style="box-sizing:border-box;"><?php echo wp_kses_post( $button_label ); ?></a>
<p class="llms-sync-action-wrap">
<a href="<?php echo esc_url( $sync_url ); ?>" class="llms-button-primary sync-action small" onclick="return confirm('<?php echo esc_js( $sync_alert ); ?>')"><?php echo wp_kses_post( $button_label ); ?></a>
</p>
<?php
return ob_get_clean();
Expand Down
27 changes: 21 additions & 6 deletions src/js/admin-certificate-editor/plugin/margins-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ function getDesc( index ) {
* @since 6.0.0
*/
const StyledTextControl = styled( TextControl )`
margin-bottom: 0;

& .components-base-control__field {
position: relative;
margin-bottom: 0;

&:hover:after,
&:focus-within:after {
right: 25px;
right: 25px;
}

&:after {
Expand All @@ -48,7 +51,8 @@ const StyledTextControl = styled( TextControl )`
pointer-events: none;
position: absolute;
right: 6px;
top: 6px;
top: 50%;
transform: translateY( -50% );
transition: right 0.05s ease-in-out;
}
}
Expand All @@ -70,7 +74,10 @@ function MarginControl( { margin, index, editMargins } ) {
marginId = [ 'top', 'right', 'bottom', 'left' ][ index ];

return (
<div style={ { flex: 1 } }>
<div
className="llms-certificate-margin-control"
style={ { flex: 1, minWidth: 0 } }
>
<StyledTextControl
id={ `llms-certificate-control--margin--${ marginId }` }
value={ currMargin }
Expand All @@ -79,7 +86,9 @@ function MarginControl( { margin, index, editMargins } ) {
editMargins( val, index, setMargin );
} }
/>
<em style={ { display: 'block', marginLeft: '4px', marginTop: '-8px' } }>{ getDesc( index ) }</em>
<em className="llms-certificate-margin-control__label">
{ getDesc( index ) }
</em>
</div>
);
}
Expand All @@ -104,11 +113,17 @@ export default function MarginsControl( { margins } ) {

return (
<BaseControl
className="llms-certificate-margins-control"
label={ __( 'Inner Margins', 'lifterlms' ) }
id="llms-certificate-margins-control"
>
<div style={ { display: 'flex' } }>
{ margins.map( ( margin, index ) => ( <MarginControl key={ index } { ...{ margin, index, editMargins } } /> ) ) }
<div style={ { display: 'flex', gap: '4px' } }>
{ margins.map( ( margin, index ) => (
<MarginControl
key={ index }
{ ...{ margin, index, editMargins } }
/>
) ) }
</div>
</BaseControl>
);
Expand Down
31 changes: 18 additions & 13 deletions src/js/admin-certificate-editor/plugin/sequential-id-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,23 @@ export default function SequentialIdControl( { sequentialId } ) {
}

return (
<TextControl
id="llms-certificate-title-control"
label={ __( 'Next Sequential ID', 'lifterlms' ) }
value={ currId }
type="number"
step="1"
min={ minSequentialId }
onChange={ ( val ) => {
setId( val );
editCertificate( 'sequential_id', val );
} }
help={ __( 'Used for the {sequential_id} merge code when generating a certificate from this template.', 'lifterlms' ) }
/>
<div className="llms-certificate-sequential-id-control">
<TextControl
id="llms-certificate-title-control"
label={ __( 'Next Sequential ID', 'lifterlms' ) }
value={ currId }
type="number"
step="1"
min={ minSequentialId }
onChange={ ( val ) => {
setId( val );
editCertificate( 'sequential_id', val );
} }
help={ __(
'Used for the {sequential_id} merge code when generating a certificate from this template.',
'lifterlms'
) }
/>
</div>
);
}
163 changes: 163 additions & 0 deletions tests/e2e/specs/admin/certificate-editor-layout.spec.js
Original file line number Diff line number Diff line change
@@ -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<number>} 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: '<!-- wp:paragraph --><p>Certificate body.</p><!-- /wp:paragraph -->',
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<void>}
*/
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 );
} );

} );
Loading