Skip to content

Commit 9ffc51e

Browse files
authored
Pricing grid redesign: style / UX fixes (#112445)
* Auto-open the storage dropdown when clicked * Add the border to feature badges * Add the border to plan savings badges * Fix the plan price currency color * Fix the crossed-out price color * Fix the top padding on plan headers * Adjust line-height and padding for plan badges * Remove the background which was overlapping with the pricing grid rounded border * Refactor the dropdown trigger to avoid race conditions
1 parent 1c3d381 commit 9ffc51e

5 files changed

Lines changed: 156 additions & 10 deletions

File tree

packages/plans-grid-next/src/components/features-grid/style.scss

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -726,8 +726,6 @@
726726
}
727727

728728
.plan-features-2023-grid__table-item {
729-
background-color: var( --studio-white );
730-
731729
&:is( td, th ) {
732730
border-left-color: #e0e0e0;
733731
}
@@ -758,7 +756,7 @@
758756
align-items: flex-start;
759757
background-color: transparent;
760758
flex-direction: column;
761-
padding: 36px 16px 0;
759+
padding: 32px 16px 0;
762760
}
763761

764762
.plan-features-2023-grid__header-title {
@@ -802,14 +800,14 @@
802800
}
803801

804802
&.is-original {
805-
color: var( --studio-gray-40 );
803+
color: var( --wp-components-color-gray-600, #949494 );
806804
font-size: 24px;
807805
margin-bottom: 3px;
808806

809807
.plan-price__currency-symbol,
810808
.plan-price__integer,
811809
.plan-price__tax-amount {
812-
color: var( --studio-gray-40 );
810+
color: var( --wp-components-color-gray-600, #949494 );
813811
}
814812

815813
.plan-price__integer {
@@ -820,6 +818,7 @@
820818

821819
.plan-price__currency-symbol,
822820
.plan-price.is-discounted .plan-price__currency-symbol {
821+
color: inherit;
823822
font-size: 14px;
824823
margin-top: 3px;
825824
}
@@ -837,6 +836,7 @@
837836
.plans-grid-next-header-price__badge {
838837
background-color: rgba( 184, 230, 191, 0.68 );
839838
border-radius: 2px;
839+
box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.08 );
840840
color: var( --studio-green-80 );
841841
font-size: 11px;
842842
font-weight: 500;
@@ -923,6 +923,7 @@
923923
}
924924

925925
.plan-features-2023-grid__feature-badge {
926+
box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.08 );
926927
flex: 0 0 auto;
927928
font-weight: 500;
928929
margin-inline-start: auto;
@@ -1036,8 +1037,8 @@
10361037
justify-content: center;
10371038
left: 16px;
10381039
letter-spacing: 0;
1039-
line-height: 16px;
1040-
padding: 0 10px;
1040+
line-height: 20px;
1041+
padding: 0 8px;
10411042
position: absolute;
10421043
right: auto;
10431044
text-transform: none;

packages/plans-grid-next/src/components/shared/storage/components/plan-storage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const PlanStorage = ( {
8080
<StorageDropdown
8181
planSlug={ planSlug }
8282
onStorageAddOnClick={ onStorageAddOnClick }
83+
openOnMount={ showFeatureCheckmarks }
8384
onStorageOptionChange={
8485
showFeatureCheckmarks ? () => setIsStorageDropdownVisible( false ) : undefined
8586
}

packages/plans-grid-next/src/components/shared/storage/components/storage-dropdown.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { type AddOnMeta, AddOns, WpcomPlansUI } from '@automattic/data-stores';
33
import { CustomSelectControl } from '@wordpress/components';
44
import { useDispatch, useSelect } from '@wordpress/data';
5-
import { useCallback, useEffect, useMemo } from '@wordpress/element';
5+
import { useCallback, useEffect, useMemo, useRef } from '@wordpress/element';
66
import { useTranslate } from 'i18n-calypso';
77
import { usePlansGridContext } from '../../../../grid-context';
88
import DropdownOption from '../../../dropdown-option';
@@ -15,6 +15,7 @@ type StorageDropdownProps = {
1515
planSlug: PlanSlug;
1616
onStorageAddOnClick?: ( addOnSlug: AddOns.StorageAddOnSlug ) => void;
1717
onStorageOptionChange?: () => void;
18+
openOnMount?: boolean;
1819
};
1920

2021
type StorageDropdownOptionProps = {
@@ -77,9 +78,12 @@ const StorageDropdown = ( {
7778
planSlug,
7879
onStorageAddOnClick,
7980
onStorageOptionChange,
81+
openOnMount,
8082
}: StorageDropdownProps ) => {
8183
const translate = useTranslate();
8284
const { siteId } = usePlansGridContext();
85+
const containerRef = useRef< HTMLDivElement >( null );
86+
const hasOpenedOnMount = useRef( false );
8387

8488
const { setSelectedStorageOptionForPlan } = useDispatch( WpcomPlansUI.store );
8589
const storageAddOns = AddOns.useStorageAddOns( { siteId } );
@@ -141,6 +145,26 @@ const StorageDropdown = ( {
141145
} );
142146
}, [ availableStorageAddOns, defaultStorageOptionSlug, planStorage, storageAddOns ] );
143147

148+
useEffect( () => {
149+
if ( ! openOnMount || hasOpenedOnMount.current || ! selectControlOptions?.length ) {
150+
return;
151+
}
152+
153+
const openDropdownTimeout = window.setTimeout( () => {
154+
const trigger = containerRef.current?.querySelector< HTMLElement >( '[role="combobox"]' );
155+
156+
if ( ! trigger ) {
157+
return;
158+
}
159+
160+
hasOpenedOnMount.current = true;
161+
trigger?.focus();
162+
trigger?.click();
163+
}, 0 );
164+
165+
return () => window.clearTimeout( openDropdownTimeout );
166+
}, [ openOnMount, selectControlOptions?.length ] );
167+
144168
const selectedStorageAddOn = getSelectedStorageAddOn(
145169
storageAddOns,
146170
selectedStorageOptionForPlan
@@ -194,7 +218,7 @@ const StorageDropdown = ( {
194218

195219
return (
196220
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
197-
<div tabIndex={ 1 }>
221+
<div ref={ containerRef } tabIndex={ 1 }>
198222
<CustomSelectControl
199223
__next40pxDefaultSize
200224
hideLabelFromVision

packages/plans-grid-next/src/components/test/plan-storage.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ jest.mock( '../shared/storage/components/storage-dropdown', () => {
1515

1616
return {
1717
__esModule: true,
18-
default: () => ReactActual.createElement( 'div', { 'data-testid': 'storage-dropdown' } ),
18+
default: ( { openOnMount } ) =>
19+
ReactActual.createElement( 'div', {
20+
'data-open-on-mount': openOnMount ? 'true' : 'false',
21+
'data-testid': 'storage-dropdown',
22+
} ),
1923
};
2024
} );
2125
jest.mock( '../shared/storage/components/storage-feature-label', () => {
@@ -72,6 +76,10 @@ describe( 'PlanStorage', () => {
7276
fireEvent.click( screen.getByRole( 'button', { name: 'Add more' } ) );
7377

7478
expect( screen.getByTestId( 'storage-dropdown' ) ).toBeInTheDocument();
79+
expect( screen.getByTestId( 'storage-dropdown' ) ).toHaveAttribute(
80+
'data-open-on-mount',
81+
'true'
82+
);
7583

7684
await new Promise( ( resolve ) => window.setTimeout( resolve, 0 ) );
7785

@@ -92,6 +100,10 @@ describe( 'PlanStorage', () => {
92100
);
93101

94102
expect( screen.getByTestId( 'storage-dropdown' ) ).toBeInTheDocument();
103+
expect( screen.getByTestId( 'storage-dropdown' ) ).toHaveAttribute(
104+
'data-open-on-mount',
105+
'false'
106+
);
95107

96108
fireEvent.click( screen.getByRole( 'button', { name: 'Outside' } ) );
97109

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
const mockComboboxClick = jest.fn();
6+
const mockSetSelectedStorageOptionForPlan = jest.fn();
7+
8+
jest.mock( '@wordpress/components', () => {
9+
const ReactActual = jest.requireActual< typeof import('react') >( 'react' );
10+
11+
return {
12+
CustomSelectControl: ( { label }: { label: string } ) =>
13+
ReactActual.createElement(
14+
'button',
15+
{
16+
'aria-label': label,
17+
onClick: mockComboboxClick,
18+
role: 'combobox',
19+
type: 'button',
20+
},
21+
'Storage options'
22+
),
23+
};
24+
} );
25+
jest.mock( '@automattic/calypso-products', () => ( {
26+
FEATURE_1GB_STORAGE: '1gb-storage',
27+
FEATURE_6GB_STORAGE: '6gb-storage',
28+
FEATURE_13GB_STORAGE: '13gb-storage',
29+
FEATURE_50GB_STORAGE: '50gb-storage',
30+
FEATURE_100GB_STORAGE: '100gb-storage',
31+
FEATURE_200GB_STORAGE: '200gb-storage',
32+
FEATURE_P2_3GB_STORAGE: 'p2-3gb-storage',
33+
FEATURE_P2_13GB_STORAGE: 'p2-13gb-storage',
34+
PLAN_BUSINESS: 'business-bundle',
35+
PLAN_ECOMMERCE: 'ecommerce-bundle',
36+
PRODUCT_1GB_SPACE: '1gb-space',
37+
} ) );
38+
jest.mock( '@wordpress/data', () => ( {
39+
useDispatch: jest.fn( () => ( {
40+
setSelectedStorageOptionForPlan: mockSetSelectedStorageOptionForPlan,
41+
} ) ),
42+
useSelect: jest.fn(),
43+
} ) );
44+
jest.mock( '@automattic/data-stores', () => ( {
45+
AddOns: {
46+
useAvailableStorageAddOns: jest.fn(),
47+
useStorageAddOns: jest.fn(),
48+
},
49+
Purchases: {
50+
useSitePurchasesByProductSlug: jest.fn(),
51+
},
52+
WpcomPlansUI: {
53+
store: {},
54+
},
55+
} ) );
56+
jest.mock( '../../grid-context', () => ( { usePlansGridContext: jest.fn() } ) );
57+
58+
import { FEATURE_50GB_STORAGE, PLAN_BUSINESS } from '@automattic/calypso-products';
59+
import { AddOns, Purchases } from '@automattic/data-stores';
60+
import { render } from '@testing-library/react';
61+
import { useSelect } from '@wordpress/data';
62+
import React from 'react';
63+
import { usePlansGridContext } from '../../grid-context';
64+
import StorageDropdown from '../shared/storage/components/storage-dropdown';
65+
66+
describe( 'StorageDropdown', () => {
67+
beforeEach( () => {
68+
jest.clearAllMocks();
69+
( AddOns.useStorageAddOns as jest.Mock ).mockReturnValue( [] );
70+
( AddOns.useAvailableStorageAddOns as jest.Mock ).mockReturnValue( [
71+
{
72+
addOnSlug: 'add-on-100gb-storage',
73+
prices: { formattedMonthlyPrice: '$10' },
74+
quantity: 100,
75+
},
76+
] );
77+
( Purchases.useSitePurchasesByProductSlug as jest.Mock ).mockReturnValue( null );
78+
( useSelect as jest.Mock ).mockReturnValue( FEATURE_50GB_STORAGE );
79+
( usePlansGridContext as jest.Mock ).mockReturnValue( {
80+
gridPlansIndex: {
81+
[ PLAN_BUSINESS ]: {
82+
features: {
83+
storageFeature: {
84+
getSlug: () => FEATURE_50GB_STORAGE,
85+
},
86+
},
87+
},
88+
},
89+
siteId: 1,
90+
} );
91+
} );
92+
93+
test( 'opens the select control after mounting when requested', async () => {
94+
render( <StorageDropdown planSlug={ PLAN_BUSINESS } openOnMount /> );
95+
96+
await new Promise( ( resolve ) => window.setTimeout( resolve, 0 ) );
97+
98+
expect( mockComboboxClick ).toHaveBeenCalledTimes( 1 );
99+
} );
100+
101+
test( 'does not open the select control after mounting by default', async () => {
102+
render( <StorageDropdown planSlug={ PLAN_BUSINESS } /> );
103+
104+
await new Promise( ( resolve ) => window.setTimeout( resolve, 0 ) );
105+
106+
expect( mockComboboxClick ).not.toHaveBeenCalled();
107+
} );
108+
} );

0 commit comments

Comments
 (0)