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
149 changes: 123 additions & 26 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Changed design to latest design patterns.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
placement = 'bottom-end',
animate = true,
iconCode = 'info-outline',
iconSize = 18,
title,
children,
} ) => {
Expand All @@ -57,7 +58,6 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
};

const args = {
iconCode,
// To be compatible with deprecating prop `position`.
position: placementsToPositions( placement ),
placement,
Expand All @@ -77,7 +77,7 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
onMouseEnter={ createToggleIsOver( 'onMouseEnter', true ) }
onMouseLeave={ createToggleIsOver( 'onMouseLeave' ) }
>
<Gridicon className={ iconClassName } icon={ args.iconCode } size={ 18 } />
<Gridicon className={ iconClassName } icon={ iconCode } size={ iconSize } />
</span>

<div className="icon-tooltip-helper">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export type IconTooltipProps = {
iconCode?: string;
title?: string;
children?: React.ReactNode;
iconSize?: number;
};
86 changes: 72 additions & 14 deletions projects/js-packages/components/components/pricing-table/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Icon, check, info, closeSmall } from '@wordpress/icons';
import { Icon, check, closeSmall } from '@wordpress/icons';
import classnames from 'classnames';
import {
createContext,
Expand All @@ -10,6 +11,8 @@ import {
ReactElement,
} from 'react';
import React, { CSSProperties } from 'react';
import { getRedirectUrl } from '../../../components';
import IconTooltip from '../icon-tooltip';
import useBreakpointMatch from '../layout/use-breakpoint-match';
import Text from '../text';
import styles from './styles.module.scss';
Expand All @@ -20,27 +23,50 @@ import {
PricingTableItemProps,
} from './types';

const ToS = createInterpolateElement(
__(
'By clicking the button above, you agree to our <tosLink>Terms of Service</tosLink> and to <shareDetailsLink>share details</shareDetailsLink> with WordPress.com.',
'jetpack'
),
{
tosLink: <a href={ getRedirectUrl( 'wpcom-tos' ) } rel="noopener noreferrer" target="_blank" />,
shareDetailsLink: (
<a
href={ getRedirectUrl( 'jetpack-support-what-data-does-jetpack-sync' ) }
rel="noopener noreferrer"
target="_blank"
/>
),
}
);

const PricingTableContext = createContext( undefined );

export const PricingTableItem: React.FC< PricingTableItemProps > = ( {
isIncluded,
index = 0,
label = null,
tooltipInfo,
tooltipTitle,
} ) => {
const [ isLg ] = useBreakpointMatch( 'lg' );
const items = useContext( PricingTableContext );
const rowLabel = items[ index ];
const rowLabel = items[ index ].name;
const defaultTooltipInfo = items[ index ].tooltipInfo;
const defaultTooltipTitle = items[ index ].tooltipTitle;
const includedLabel = __( 'Included', 'jetpack' );
const notIncludedLabel = __( 'Not included', 'jetpack' );
const showTooltip = tooltipInfo || ( ! isLg && defaultTooltipInfo );

let defaultLabel = isIncluded ? includedLabel : notIncludedLabel;
defaultLabel = isLg ? defaultLabel : rowLabel;

if ( ! isLg && ! isIncluded ) {
if ( ! isLg && ! isIncluded && label === null ) {
return null;
}

return (
<Text variant="body-small" className={ classnames( styles.item, styles.value ) }>
<div className={ classnames( styles.item, styles.value ) }>
<Icon
className={ classnames(
styles.icon,
Expand All @@ -49,20 +75,34 @@ export const PricingTableItem: React.FC< PricingTableItemProps > = ( {
size={ 32 }
icon={ isIncluded ? check : closeSmall }
/>
{ label || defaultLabel }
</Text>
<Text variant="body-small">{ label || defaultLabel }</Text>
{ showTooltip && (
<IconTooltip
title={ tooltipInfo ? tooltipTitle : defaultTooltipTitle }
iconClassName={ styles[ 'popover-icon' ] }
className={ styles.popover }
placement={ 'bottom-end' }
iconSize={ 22 }
>
<Text>{ tooltipInfo || defaultTooltipInfo }</Text>
</IconTooltip>
) }
</div>
);
};

export const PricingTableHeader: React.FC< PricingTableHeaderProps > = ( { children } ) => (
<div className={ styles.header }>{ children }</div>
);

export const PricingTableColumn: React.FC< PricingTableColumnProps > = ( { children } ) => {
export const PricingTableColumn: React.FC< PricingTableColumnProps > = ( {
primary = false,
children,
} ) => {
let index = 0;

return (
<div className={ styles.card }>
<div className={ classnames( styles.card, { [ styles[ 'is-primary' ] ]: primary } ) }>
{ Children.map( children, child => {
const item = child as ReactElement<
PropsWithChildren< PricingTableHeaderProps | PricingTableItemProps >
Expand Down Expand Up @@ -97,17 +137,35 @@ const PricingTable: React.FC< PricingTableProps > = ( { title, items, children }
<Text variant="headline-small">{ title }</Text>
{ isLg &&
items.map( ( item, i ) => (
<Text
variant="body-small"
className={ classnames( styles.item, styles.label ) }
<div
className={ classnames( styles.item, {
[ styles[ 'last-feature' ] ]: i === items.length - 1,
} ) }
key={ i }
>
<Icon className={ classnames( styles.icon ) } size={ 24 } icon={ info } />
<strong>{ item }</strong>
</Text>
<Text variant="body-small">
<strong>{ item.name }</strong>
</Text>
{ item.tooltipInfo && (
<IconTooltip
title={ item.tooltipTitle }
iconClassName={ styles[ 'popover-icon' ] }
className={ styles.popover }
placement={ 'bottom-end' }
iconSize={ 22 }
>
<Text>{ item.tooltipInfo }</Text>
</IconTooltip>
) }
</div>
) ) }
{ children }
</div>
<div className={ styles[ 'tos-container' ] }>
<Text className={ styles.tos } variant="body-small">
{ ToS }
</Text>
</div>
</div>
</PricingTableContext.Provider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,24 @@ export default {

const Template: ComponentStory< typeof PricingTable > = args => (
<PricingTable { ...args }>
<PricingTableColumn>
<PricingTableColumn primary>
<PricingTableHeader>
<ProductPrice
price={ 9.95 }
offPrice={ 4.98 }
leyend="/month, billed yearly"
currency="USD"
promoLabel="50% off"
/>
<Button fullWidth>Get Premium</Button>
</PricingTableHeader>
<PricingTableItem isIncluded={ true } label={ <strong>Up to 1000</strong> } />
<PricingTableItem isIncluded={ true } />
<PricingTableItem isIncluded={ true } />
<PricingTableItem isIncluded={ true } tooltipInfo={ 'This is an info' } />
<PricingTableItem
isIncluded={ true }
tooltipInfo={ 'This is an info with title' }
tooltipTitle={ 'Small title' }
/>
<PricingTableItem isIncluded={ true } />
<PricingTableItem isIncluded={ true } />
</PricingTableColumn>
Expand All @@ -35,22 +40,30 @@ const Template: ComponentStory< typeof PricingTable > = args => (
</Button>
</PricingTableHeader>
<PricingTableItem isIncluded={ true } label="Up to 300" />
<PricingTableItem
isIncluded={ false }
label="This is not included"
tooltipInfo="This has a tooltip, so its overwrites the default info on small screens"
/>
<PricingTableItem isIncluded={ false } />
<PricingTableItem isIncluded={ true } />
<PricingTableItem isIncluded={ true } />
<PricingTableItem isIncluded={ true } />
<PricingTableItem isIncluded={ false } />
</PricingTableColumn>
</PricingTable>
);

const DefaultArgs = {
title: 'Buy premium, or start for free',
items: [
'Feature A with limit',
'Feature B',
'Feature C with a longer title that will span multiple lines',
'Feature D',
'Feature E',
{ name: 'Feature A with limit', tooltipInfo: 'Default info for Feature A' },
{ name: 'Feature B', tooltipInfo: 'Default info for Feature B' },
{
name: 'Feature C with a longer title that will span multiple lines',
tooltipInfo: 'Default info for Feature C',
tooltipTitle: 'Title for C',
},
{ name: 'Feature D', tooltipInfo: 'Default info for Feature D', tooltipTitle: 'Title for D' },
{ name: 'Feature E' },
],
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.container {
--padding: calc( var( --spacing-base ) * 4 );
color: var( --jp-black );

}

.table {
Expand All @@ -15,6 +16,7 @@
grid-auto-flow: column;
grid-template-rows: repeat( var( --rows ), minmax( min-content, max-content ) );
column-gap: var( --gap );
overflow: hidden;
}
}

Expand All @@ -25,21 +27,28 @@
display: contents;
}

> * {
background: var( --jp-white );
border: solid var( --jp-gray );
border-width: 0 1px;
box-shadow: 0px 4px 24px rgba( 0, 0, 0, 0.05 );
clip-path: inset(
0px -24px -24px -24px
); // Offset clip-path everywhere but the top to display the shadows.
&.is-primary {
> * {
background: var( --jp-white );
position: relative;

&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
box-shadow: 0px 4px 24px rgba( 0, 0, 0, 0.05 );
}
}
}

> :first-child {
border-top-left-radius: var( --jp-border-radius );
border-top-right-radius: var( --jp-border-radius );
border-width: 1px 1px 0;
clip-path: none; // Allow shadows everywhere.
}

> :last-child {
Expand All @@ -59,16 +68,18 @@
padding-bottom: calc( var( --spacing-base ) * 2 );
position: relative;

&:not( :first-of-type ) {
&:not( :nth-child(2) ) {
padding-top: calc( var( --spacing-base ) * 2 );

&::before {
content: '';
position: absolute;
top: 0;
left: var( --border-offset, 0 );
right: var( --border-offset, 0 );
height: 1px;
.is-viewport-large & {
width: 150%;
}
z-index: 5;
background-color: var( --jp-gray );
}
}
Expand All @@ -78,12 +89,11 @@
}
}

.label {
margin-right: var( --padding );
.last-feature {
padding-bottom: var( --padding );
}

.value {
--border-offset: var( --padding );
padding-left: var( --padding );
padding-right: var( --padding );
}
Expand All @@ -101,3 +111,43 @@
.icon-cross {
--fill: var( --jp-red-50 );
}

.popover {
margin-left: auto;

.is-viewport-large & {
top: 1px;
margin: 0 var( --spacing-base );
}
}

.popover-icon {
fill: var( --fill, var( --jp-gray ) );
flex-shrink: 0;
}

.tos {
text-align: center;
width: fit-content;

> a {
color: black;
}

.is-viewport-large & {
padding-left: var( --padding );
padding-right: var( --padding );
grid-column: 2;
white-space: nowrap;
overflow: hidden;
}
}

.tos-container {
.is-viewport-large & {
display: grid;
grid-template-columns: repeat( var( --columns ), 1fr );
grid-auto-flow: column;
column-gap: var( --gap );
}
}
Loading