Skip to content
Merged
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
15 changes: 11 additions & 4 deletions e2e-tests/specs/onboarding.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ test.describe('Onboarding', () => {
await expect(page.locator('.ob-error-wrap')).toHaveCount(0);
};

const featureCard = ( page, pluginSlug ) =>
page.locator(`.ob-feature-header[data-plugin="${ pluginSlug }"] .ob-feature-select`);

test('Sub-menu in Admin page', async ({ page, admin }) => {
await admin.visitAdminPage('/');

Expand Down Expand Up @@ -110,13 +113,17 @@ test.describe('Onboarding', () => {
await openFirstSiteAndWaitForData( page );
await page.getByRole('button', { name: 'Continue' }).click();

expect(await page.locator('.ob-feature-card').count()).toBe(6);
// FeaturesList caps the list at MAX_FEATURE_LIST_LENGTH (6).
const featureCardCount = await page.locator('.ob-feature-card').count();
expect(featureCardCount).toBeGreaterThanOrEqual(5);
expect(featureCardCount).toBeLessThanOrEqual(6);
expect(
await page.locator('.ob-feature-card.ob-disabled[aria-checked="true"]').count(),
await page.locator('.ob-feature-card.ob-disabled .ob-feature-select[aria-checked="true"]').count(),
).toBeGreaterThan(0); // We have some required plugin that are active by default.

// Check if we can select a plugin to install.
const cachePlugin = page.getByRole('checkbox', { name: 'Caching Supercharge your site' });
const cachePlugin = featureCard( page, 'wp-cloudflare-page-cache' );
await expect(cachePlugin).toHaveAttribute('aria-checked', 'false');
await cachePlugin.click();
await expect(cachePlugin).toHaveAttribute('aria-checked', 'true');

Expand All @@ -133,7 +140,7 @@ test.describe('Onboarding', () => {
await admin.visitAdminPage(ONBOARDING_URL);
await openFirstSiteAndWaitForData( page );
await page.getByRole('button', { name: 'Continue' }).click();
const cachePlugin = page.getByRole('checkbox', { name: 'Caching Supercharge your site' });
const cachePlugin = featureCard( page, 'wp-cloudflare-page-cache' );
await cachePlugin.click();
await page.getByRole('button', { name: 'Import Website' }).click();

Expand Down
126 changes: 98 additions & 28 deletions onboarding/src/Components/FeaturesList.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from '@wordpress/element';
import { useState, useEffect, useLayoutEffect, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { decodeHtmlEntities } from '../utils/common';

Expand All @@ -12,13 +12,7 @@ const featuredPluginCollection = [
id: 'pageBuilder',
pluginSlug: 'otter-blocks',
label: __('Site Builder', 'templates-patterns-collection'),
description: __('Build beautiful pages with a simple drag-and-drop page builder.', 'templates-patterns-collection')
},
{
id: 'contactForm',
pluginSlug: 'otter-blocks',
label: __('Contact Form', 'templates-patterns-collection'),
description: __('Create forms to capture leads and feedback.', 'templates-patterns-collection')
description: __('Build pages and forms with Otter.', 'templates-patterns-collection')
},
{
id: 'imageOpt',
Expand Down Expand Up @@ -123,7 +117,6 @@ const FeaturesList = ({ requiredPlugins, onToggle }) => {

const [selectedFeatures, setSelectedFeatures] = useState({
pageBuilder: false,
contactForm: false,
eCommerce: false,
donations: false,
automation: false,
Expand All @@ -133,6 +126,19 @@ const FeaturesList = ({ requiredPlugins, onToggle }) => {
});

const [lockedPluginSlugs, setLockedPluginSlugs] = useState([]);
const [expandedFeatures, setExpandedFeatures] = useState({});

const gridRef = useRef(null);
const autoFitApplied = useRef(false);

const toggleExpanded = (feature) => {
// Any manual toggle takes over from the automatic fit.
autoFitApplied.current = true;
setExpandedFeatures((prev) => ({
...prev,
[feature]: !prev[feature],
}));
};

const toggleFeature = (feature, pluginSlug) => {
if (lockedPluginSlugs.includes(pluginSlug)) {
Expand Down Expand Up @@ -198,41 +204,105 @@ const FeaturesList = ({ requiredPlugins, onToggle }) => {

setFeatureList(orderedFeatures);
setLockedPluginSlugs(requiredPluginSlugs);
setExpandedFeatures(
Object.fromEntries(orderedFeatures.map(({ id }) => [id, true]))
);
autoFitApplied.current = false;
}, [requiredPlugins]);

// Descriptions start open and collapse only if the list would run past the footer.
// Selected features give up their description first, since unselected ones still need the pitch.
useLayoutEffect(() => {
if (autoFitApplied.current || !gridRef.current || 0 === featureList.length) {
return;
}
autoFitApplied.current = true;

const footer = document.querySelector('.ob-settings-bottom');
const limit = window.innerHeight - (footer ? footer.offsetHeight : 0);
const overflow = gridRef.current.getBoundingClientRect().bottom - limit;

if (overflow <= 0) {
return;
}

const heightOf = (id) => {
const description = gridRef.current.querySelector(`#ob-feature-desc-${id}`);
if (!description || description.hidden) {
return 0;
}
return description.offsetHeight + parseFloat(window.getComputedStyle(description).marginTop || 0);
};

const isSelected = ({ id, pluginSlug }) => selectedFeatures[id] || lockedPluginSlugs.includes(pluginSlug);
const reclaimed = featureList.filter(isSelected).reduce((total, { id }) => total + heightOf(id), 0);

setExpandedFeatures(
reclaimed >= overflow
? Object.fromEntries(featureList.filter((feature) => !isSelected(feature)).map(({ id }) => [id, true]))
: {}
);
}, [featureList]);

return (
<div className="ob-select-features">
<div className="ob-features-grid">
<div className="ob-features-grid" ref={gridRef}>
{
featureList.map((feature) => {
const checked = selectedFeatures[feature.id] || lockedPluginSlugs.includes(feature.pluginSlug);
const isLocked = lockedPluginSlugs.includes(feature.pluginSlug);
const isExpanded = Boolean(expandedFeatures[feature.id]);
const titleId = `ob-feature-title-${feature.id}`;
const descriptionId = `ob-feature-desc-${feature.id}`;
return (
<button
<div
key={feature.id}
className={`ob-feature-card ${
checked ? 'selected' : ''
} ${isLocked ? 'ob-disabled' : ''}`}
onClick={() => toggleFeature(feature.id, feature.pluginSlug)}
onKeyPress={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
toggleFeature(feature.id, feature.pluginSlug);
}
}}
role="checkbox"
aria-checked={checked}
disabled={isLocked}
>
<div className="ob-feature-header" data-plugin={feature.pluginSlug}>
<h4 className="ob-feature-title">{feature.label}</h4>
<input
type="checkbox"
checked={checked}
readOnly
/>
<button
type="button"
className="ob-feature-select"
onClick={() => toggleFeature(feature.id, feature.pluginSlug)}
role="checkbox"
aria-checked={checked}
disabled={isLocked}
>
<input
type="checkbox"
checked={checked}
readOnly
tabIndex={-1}
/>
<h4 className="ob-feature-title" id={titleId}>{feature.label}</h4>
</button>
{feature.description && (
<button
type="button"
className="ob-feature-expand"
onClick={() => toggleExpanded(feature.id)}
aria-expanded={isExpanded}
aria-controls={descriptionId}
aria-labelledby={titleId}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
<path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
)}
</div>
<div className="ob-feature-description">{feature.description}</div>
</button>
{feature.description && (
<div
className="ob-feature-description"
id={descriptionId}
hidden={!isExpanded}
>
{feature.description}
</div>
)}
</div>
);
})
}
Expand Down
3 changes: 2 additions & 1 deletion onboarding/src/Components/SiteSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ export const SiteSettings = ( {
<div
className={ classnames(
'ob-site-settings',
fetching ? 'fetching' : ''
fetching ? 'fetching' : '',
step === 4 && canImport ? 'is-step-4-features' : ''
) }
>
{ ! fetching ? (
Expand Down
Loading
Loading