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 (
-
+
{ 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 ( - + {feature.description && ( + + )}
-
{feature.description}
- + {feature.description && ( + + )} +
); }) } diff --git a/onboarding/src/Components/SiteSettings.js b/onboarding/src/Components/SiteSettings.js index bd2d5527..b75be1a2 100644 --- a/onboarding/src/Components/SiteSettings.js +++ b/onboarding/src/Components/SiteSettings.js @@ -146,7 +146,8 @@ export const SiteSettings = ( {
{ ! fetching ? ( diff --git a/onboarding/src/scss/_features_list.scss b/onboarding/src/scss/_features_list.scss index a2a35a81..231c0733 100644 --- a/onboarding/src/scss/_features_list.scss +++ b/onboarding/src/scss/_features_list.scss @@ -1,47 +1,67 @@ .ob-select-features { display: flex; flex-direction: column; + width: 100%; .ob-features-grid { display: flex; flex-direction: column; - gap: 1rem; + gap: 10px; width: 100%; + min-width: 0; } .ob-feature-card { display: flex; flex-direction: column; align-items: flex-start; - padding: 1rem; + width: 100%; + min-width: 0; + box-sizing: border-box; + padding: 12px 14px; background-color: #ffffff; - border: 1px solid #e0e0e0; - border-radius: 6px; - cursor: pointer; + border: 1px solid #e3e8ef; + border-radius: 10px; transition: box-shadow 0.3s ease, border 0.3s ease; + text-align: left; &:hover:not(.ob-disabled) { - border-color: #007cba; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-color: #9ec3ef; + box-shadow: 0 2px 8px rgba(3, 102, 213, 0.08); } - &:focus { + &:focus-within { outline: none; - border-color: #007cba; - box-shadow: 0 0 0 4px rgba(0, 124, 186, 0.2); + border-color: #0366d5; + box-shadow: 0 0 0 3px rgba(3, 102, 213, 0.18); } &.selected { - border-color: #007cba; - background-color: #e8f4fa; + border-color: #8bb6f3; + background-color: #eaf3ff; + + .ob-feature-title { + color: #0f4fbf; + } } &.ob-disabled { cursor: not-allowed; - opacity: 0.6; + opacity: 1; + border-color: #dce3ee; + background-color: #f7f9fc; + + .ob-feature-title { + color: #7b8697; + } + + input[type='checkbox'] { + cursor: not-allowed; + opacity: 0.7; + } &:hover { - border-color: #e0e0e0; + border-color: #dce3ee; box-shadow: none; } } @@ -49,29 +69,86 @@ .ob-feature-header { display: flex; align-items: center; - justify-content: space-between; + gap: 8px; width: 100%; + .ob-feature-select { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; + padding: 0; + background: none; + border: 0; + cursor: pointer; + text-align: left; + + &:focus { + outline: none; + } + } + .ob-feature-title { - font-size: 1rem; - font-weight: bold; - color: #333333; + font-size: 16px; + line-height: 1.2; + font-weight: 700; + color: #1f2937; margin: 0; + flex: 1; text-align: left; + min-width: 0; } input[type='checkbox'] { cursor: pointer; - accent-color: #007cba; - transform: scale(1.2); + accent-color: #2b62de; + margin: 0; + width: 18px; + height: 18px; + flex-shrink: 0; + } + + .ob-feature-expand { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 28px; + height: 28px; + margin-right: -6px; + padding: 0; + background: none; + border: 0; + border-radius: 4px; + color: #9aa3b2; + cursor: pointer; + + &:hover { + color: #0f4fbf; + background-color: rgba(3, 102, 213, 0.08); + } + + svg { + transition: transform 0.2s ease; + } + + &[aria-expanded='true'] svg { + transform: rotate(180deg); + } } } .ob-feature-description { - font-size: 0.875rem; + font-size: 14px; + line-height: 1.35; color: #666666; - margin-top: 0.5rem; + margin-top: 6px; text-align: left; + overflow-wrap: anywhere; + + // Align with the title rather than the leading checkbox. + padding-left: 28px; } } -} \ No newline at end of file +} diff --git a/onboarding/src/scss/_site-settings.scss b/onboarding/src/scss/_site-settings.scss index 30b4556c..c7fdbbc8 100644 --- a/onboarding/src/scss/_site-settings.scss +++ b/onboarding/src/scss/_site-settings.scss @@ -96,4 +96,26 @@ padding: 20px; background: #fff; box-shadow: 4px 4px 40px 0 rgba(0, 0, 0, 0.10); -} \ No newline at end of file +} + +.ob-site-settings.is-step-4-features { + .ob-settings-description { + padding-top: 16px; + gap: 8px; + margin-bottom: 20px; + + p { + margin: 0; + font-size: 15px; + line-height: 1.45; + } + } + + .ob-settings-top { + gap: 16px; + } + + .ob-settings-bottom { + padding: 14px 20px 16px; + } +}