diff --git a/e2e-tests/specs/onboarding.spec.js b/e2e-tests/specs/onboarding.spec.js index 604409cf..c5a2fc82 100644 --- a/e2e-tests/specs/onboarding.spec.js +++ b/e2e-tests/specs/onboarding.spec.js @@ -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('/'); @@ -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'); @@ -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(); diff --git a/onboarding/src/Components/FeaturesList.js b/onboarding/src/Components/FeaturesList.js index 411c41b4..679f49f1 100644 --- a/onboarding/src/Components/FeaturesList.js +++ b/onboarding/src/Components/FeaturesList.js @@ -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'; @@ -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', @@ -123,7 +117,6 @@ const FeaturesList = ({ requiredPlugins, onToggle }) => { const [selectedFeatures, setSelectedFeatures] = useState({ pageBuilder: false, - contactForm: false, eCommerce: false, donations: false, automation: false, @@ -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)) { @@ -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 (