@automattic/jetpack-components 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/.gitattributes +8 -0
  2. package/CHANGELOG.md +747 -0
  3. package/LICENSE.txt +357 -0
  4. package/README.md +11 -0
  5. package/SECURITY.md +38 -0
  6. package/components/action-button/index.jsx +70 -0
  7. package/components/action-button/stories/index.stories.jsx +38 -0
  8. package/components/action-button/style.module.scss +11 -0
  9. package/components/action-button/test/component.jsx +16 -0
  10. package/components/action-popover/index.tsx +105 -0
  11. package/components/action-popover/stories/index.stories.jsx +115 -0
  12. package/components/action-popover/stories/styles.module.scss +7 -0
  13. package/components/action-popover/styles.module.scss +49 -0
  14. package/components/action-popover/types.ts +20 -0
  15. package/components/admin-page/index.tsx +58 -0
  16. package/components/admin-page/stories/index.stories.tsx +35 -0
  17. package/components/admin-page/stories/style.module.scss +13 -0
  18. package/components/admin-page/style.module.scss +11 -0
  19. package/components/admin-page/types.ts +41 -0
  20. package/components/admin-section/basic/index.tsx +15 -0
  21. package/components/admin-section/basic/style.module.scss +3 -0
  22. package/components/admin-section/hero/index.tsx +15 -0
  23. package/components/admin-section/hero/style.module.scss +3 -0
  24. package/components/admin-section/stories/index.stories.tsx +60 -0
  25. package/components/admin-section/types.ts +6 -0
  26. package/components/alert/index.tsx +61 -0
  27. package/components/alert/stories/index.stories.jsx +24 -0
  28. package/components/alert/style.module.scss +43 -0
  29. package/components/automattic-byline-logo/README.md +16 -0
  30. package/components/automattic-byline-logo/index.tsx +38 -0
  31. package/components/automattic-byline-logo/stories/index.stories.tsx +20 -0
  32. package/components/automattic-byline-logo/test/component.tsx +28 -0
  33. package/components/automattic-byline-logo/types.ts +18 -0
  34. package/components/boost-score-bar/index.tsx +103 -0
  35. package/components/boost-score-bar/stories/index.stories.tsx +39 -0
  36. package/components/boost-score-bar/style.scss +184 -0
  37. package/components/boost-score-bar/test/component.tsx +102 -0
  38. package/components/boost-score-bar/types.ts +9 -0
  39. package/components/button/index.tsx +84 -0
  40. package/components/button/stories/index.mdx +90 -0
  41. package/components/button/stories/index.stories.tsx +400 -0
  42. package/components/button/stories/style.module.scss +20 -0
  43. package/components/button/style.module.scss +212 -0
  44. package/components/button/types.ts +21 -0
  45. package/components/contextual-upgrade-trigger/README.md +81 -0
  46. package/components/contextual-upgrade-trigger/index.tsx +44 -0
  47. package/components/contextual-upgrade-trigger/stories/index.stories.tsx +25 -0
  48. package/components/contextual-upgrade-trigger/style.module.scss +75 -0
  49. package/components/contextual-upgrade-trigger/types.ts +9 -0
  50. package/components/decorative-card/index.tsx +44 -0
  51. package/components/decorative-card/stories/index.stories.tsx +18 -0
  52. package/components/decorative-card/style.scss +116 -0
  53. package/components/decorative-card/types.ts +16 -0
  54. package/components/dialog/index.tsx +69 -0
  55. package/components/dialog/stories/boost.png +0 -0
  56. package/components/dialog/stories/declaration.d.ts +1 -0
  57. package/components/dialog/stories/index.stories.tsx +118 -0
  58. package/components/dialog/stories/js-components.components.dialog.mdx +62 -0
  59. package/components/dialog/stories/js-components.components.dialog.stories.js +37 -0
  60. package/components/dialog/stories/style.module.scss +21 -0
  61. package/components/dialog/style.module.scss +30 -0
  62. package/components/donut-meter/index.tsx +147 -0
  63. package/components/donut-meter/stories/index.stories.tsx +51 -0
  64. package/components/donut-meter/style.scss +11 -0
  65. package/components/donut-meter/test/component.tsx +24 -0
  66. package/components/gridicon/index.tsx +298 -0
  67. package/components/gridicon/stories/index.stories.tsx +18 -0
  68. package/components/gridicon/style.scss +16 -0
  69. package/components/gridicon/types.ts +48 -0
  70. package/components/icon-tooltip/index.tsx +96 -0
  71. package/components/icon-tooltip/stories/index.stories.tsx +108 -0
  72. package/components/icon-tooltip/stories/style.scss +36 -0
  73. package/components/icon-tooltip/style.scss +121 -0
  74. package/components/icon-tooltip/test/component.tsx +15 -0
  75. package/components/icon-tooltip/types.ts +76 -0
  76. package/components/icons/README.md +98 -0
  77. package/components/icons/index.tsx +369 -0
  78. package/components/icons/stories/index.stories.tsx +65 -0
  79. package/components/icons/stories/style.module.scss +33 -0
  80. package/components/icons/style.module.scss +40 -0
  81. package/components/icons/types.ts +38 -0
  82. package/components/indeterminate-progress-bar/index.tsx +28 -0
  83. package/components/indeterminate-progress-bar/stories/index.stories.tsx +14 -0
  84. package/components/indeterminate-progress-bar/style.module.scss +34 -0
  85. package/components/indeterminate-progress-bar/types.ts +6 -0
  86. package/components/jetpack-footer/README.md +21 -0
  87. package/components/jetpack-footer/index.tsx +94 -0
  88. package/components/jetpack-footer/stories/index.stories.tsx +40 -0
  89. package/components/jetpack-footer/style.scss +116 -0
  90. package/components/jetpack-footer/test/component.tsx +45 -0
  91. package/components/jetpack-footer/types.ts +36 -0
  92. package/components/jetpack-logo/README.md +20 -0
  93. package/components/jetpack-logo/index.tsx +46 -0
  94. package/components/jetpack-logo/stories/index.stories.tsx +20 -0
  95. package/components/jetpack-logo/test/component.tsx +16 -0
  96. package/components/jetpack-logo/types.ts +7 -0
  97. package/components/jetpack-search-logo/README.md +20 -0
  98. package/components/jetpack-search-logo/index.tsx +93 -0
  99. package/components/jetpack-search-logo/stories/index.stories.tsx +21 -0
  100. package/components/jetpack-search-logo/test/component.tsx +16 -0
  101. package/components/jetpack-search-logo/types.ts +7 -0
  102. package/components/jetpack-vaultpress-backup-logo/README.md +19 -0
  103. package/components/jetpack-vaultpress-backup-logo/index.tsx +113 -0
  104. package/components/jetpack-vaultpress-backup-logo/stories/index.stories.tsx +20 -0
  105. package/components/jetpack-vaultpress-backup-logo/test/component.tsx +18 -0
  106. package/components/jetpack-vaultpress-backup-logo/types.ts +6 -0
  107. package/components/jetpack-videopress-logo/README.md +19 -0
  108. package/components/jetpack-videopress-logo/index.tsx +93 -0
  109. package/components/jetpack-videopress-logo/stories/index.stories.tsx +20 -0
  110. package/components/jetpack-videopress-logo/test/component.tsx +16 -0
  111. package/components/jetpack-videopress-logo/types.tsx +6 -0
  112. package/components/layout/README.md +9 -0
  113. package/components/layout/breakpoints.module.scss +20 -0
  114. package/components/layout/col/README.md +81 -0
  115. package/components/layout/col/index.tsx +49 -0
  116. package/components/layout/col/style.module.scss +28 -0
  117. package/components/layout/container/README.md +77 -0
  118. package/components/layout/container/index.tsx +42 -0
  119. package/components/layout/container/style.module.scss +35 -0
  120. package/components/layout/stories/index.stories.tsx +145 -0
  121. package/components/layout/stories/styles.module.scss +22 -0
  122. package/components/layout/types.ts +55 -0
  123. package/components/layout/use-breakpoint-match/README.md +43 -0
  124. package/components/layout/use-breakpoint-match/declaration.d.ts +9 -0
  125. package/components/layout/use-breakpoint-match/index.ts +79 -0
  126. package/components/loading-placeholder/README.md +22 -0
  127. package/components/loading-placeholder/index.tsx +23 -0
  128. package/components/loading-placeholder/stories/index.stories.tsx +78 -0
  129. package/components/loading-placeholder/style.module.scss +18 -0
  130. package/components/notice/index.tsx +98 -0
  131. package/components/notice/stories/index.stories.jsx +90 -0
  132. package/components/notice/style.module.scss +107 -0
  133. package/components/number-format/README.md +25 -0
  134. package/components/number-format/index.ts +16 -0
  135. package/components/pricing-card/index.tsx +127 -0
  136. package/components/pricing-card/stories/index.stories.tsx +55 -0
  137. package/components/pricing-card/style.scss +112 -0
  138. package/components/pricing-card/test/component.tsx +89 -0
  139. package/components/pricing-card/types.ts +52 -0
  140. package/components/pricing-slider/index.tsx +53 -0
  141. package/components/pricing-slider/stories/index.stories.tsx +44 -0
  142. package/components/pricing-slider/style.scss +55 -0
  143. package/components/pricing-slider/test/component.tsx +14 -0
  144. package/components/pricing-slider/types.ts +58 -0
  145. package/components/pricing-table/index.tsx +200 -0
  146. package/components/pricing-table/stories/index.stories.tsx +74 -0
  147. package/components/pricing-table/styles.module.scss +160 -0
  148. package/components/pricing-table/test/component.tsx +41 -0
  149. package/components/pricing-table/types.ts +84 -0
  150. package/components/product-offer/icons-card.tsx +47 -0
  151. package/components/product-offer/index.tsx +115 -0
  152. package/components/product-offer/product-offer-header.tsx +23 -0
  153. package/components/product-offer/stories/index.stories.tsx +104 -0
  154. package/components/product-offer/style.module.scss +93 -0
  155. package/components/product-offer/types.ts +125 -0
  156. package/components/product-price/index.tsx +90 -0
  157. package/components/product-price/price.tsx +35 -0
  158. package/components/product-price/stories/index.stories.tsx +33 -0
  159. package/components/product-price/style.module.scss +69 -0
  160. package/components/product-price/test/component.tsx +26 -0
  161. package/components/product-price/types.ts +73 -0
  162. package/components/qr-code/Readme.md +5 -0
  163. package/components/qr-code/index.tsx +78 -0
  164. package/components/qr-code/stories/index.stories.tsx +32 -0
  165. package/components/record-meter-bar/index.tsx +135 -0
  166. package/components/record-meter-bar/stories/index.stories.tsx +39 -0
  167. package/components/record-meter-bar/style.scss +38 -0
  168. package/components/record-meter-bar/test/component.tsx +52 -0
  169. package/components/spinner/README.md +28 -0
  170. package/components/spinner/index.jsx +45 -0
  171. package/components/spinner/stories/index.stories.jsx +20 -0
  172. package/components/spinner/style.scss +31 -0
  173. package/components/spinner/test/component.jsx +17 -0
  174. package/components/split-button/index.tsx +39 -0
  175. package/components/split-button/stories/index.stories.tsx +35 -0
  176. package/components/split-button/style.module.scss +21 -0
  177. package/components/split-button/types.ts +14 -0
  178. package/components/stat-card/index.tsx +49 -0
  179. package/components/stat-card/stories/index.mdx +35 -0
  180. package/components/stat-card/stories/index.stories.tsx +41 -0
  181. package/components/stat-card/style.module.scss +49 -0
  182. package/components/stat-card/test/component.tsx +24 -0
  183. package/components/stat-card/types.ts +28 -0
  184. package/components/status/README.md +42 -0
  185. package/components/status/index.tsx +40 -0
  186. package/components/status/stories/index.stories.tsx +44 -0
  187. package/components/status/style.module.scss +72 -0
  188. package/components/terms-of-service/index.tsx +63 -0
  189. package/components/terms-of-service/styles.scss +10 -0
  190. package/components/terms-of-service/test/index.test.jsx +52 -0
  191. package/components/terms-of-service/types.ts +30 -0
  192. package/components/testimonials/index.tsx +58 -0
  193. package/components/testimonials/style.scss +127 -0
  194. package/components/testimonials/test/component.tsx +103 -0
  195. package/components/testimonials/testimonial.tsx +47 -0
  196. package/components/testimonials/types.ts +17 -0
  197. package/components/text/constants.ts +30 -0
  198. package/components/text/index.tsx +91 -0
  199. package/components/text/stories/headings.index.stories.tsx +80 -0
  200. package/components/text/stories/index.mdx +195 -0
  201. package/components/text/stories/index.stories.tsx +69 -0
  202. package/components/text/stories/style.module.scss +64 -0
  203. package/components/text/stories/title.index.stories.tsx +34 -0
  204. package/components/text/style.module.scss +125 -0
  205. package/components/text/types.ts +53 -0
  206. package/components/theme-provider/Readme.md +49 -0
  207. package/components/theme-provider/globals.module.scss +6 -0
  208. package/components/theme-provider/index.tsx +151 -0
  209. package/components/theme-provider/stories/index.stories.tsx +164 -0
  210. package/components/theme-provider/stories/style.module.scss +79 -0
  211. package/components/theme-provider/types.ts +25 -0
  212. package/components/toggle-control/README.md +5 -0
  213. package/components/toggle-control/index.tsx +67 -0
  214. package/components/toggle-control/stories/index.stories.tsx +83 -0
  215. package/components/toggle-control/styles.module.scss +41 -0
  216. package/components/zendesk-chat/README.md +26 -0
  217. package/components/zendesk-chat/constants.ts +2 -0
  218. package/components/zendesk-chat/index.tsx +22 -0
  219. package/components/zendesk-chat/stories/index.stories.tsx +15 -0
  220. package/components/zendesk-chat/test/component.tsx +16 -0
  221. package/components/zendesk-chat/types.ts +3 -0
  222. package/index.ts +69 -0
  223. package/lib/locale/index.ts +48 -0
  224. package/lib/locale/test/index.ts +21 -0
  225. package/package.json +80 -0
  226. package/tools/get-product-checkout-url/Readme.md +32 -0
  227. package/tools/get-product-checkout-url/index.ts +31 -0
  228. package/tools/jp-redirect/README.md +61 -0
  229. package/tools/jp-redirect/index.ts +62 -0
  230. package/tools/jp-redirect/test/index.tsx +89 -0
  231. package/tools/jp-redirect/types.ts +49 -0
  232. package/tools/pricing-utils/README.md +16 -0
  233. package/tools/pricing-utils/index.ts +11 -0
  234. package/tools/pricing-utils/test/index.ts +39 -0
  235. package/tools/pricing-utils/types.ts +12 -0
@@ -0,0 +1,53 @@
1
+ import classNames from 'classnames';
2
+ import React from 'react';
3
+ import ReactSlider from 'react-slider';
4
+ import { PricingSliderProps } from './types';
5
+ import './style.scss';
6
+
7
+ /**
8
+ * Generate Pricing Slider
9
+ * More support from the original ReactSlider component: https://zillow.github.io/react-slider/
10
+ *
11
+ * @param {PricingSliderProps} props - Props
12
+ * @returns {React.ReactElement} - JSX element
13
+ */
14
+ const PricingSlider: React.FC< PricingSliderProps > = ( {
15
+ className,
16
+ maxValue = 100,
17
+ minValue = 0,
18
+ step = 1,
19
+ value,
20
+ onChange,
21
+ onBeforeChange,
22
+ onAfterChange,
23
+ renderThumb,
24
+ } ) => {
25
+ const componentClassName = classNames( 'jp-components-pricing-slider', className );
26
+
27
+ const renderThumbCallback = renderThumb
28
+ ? renderThumb
29
+ : ( props, state ) => {
30
+ return <div { ...props }>{ state.valueNow }</div>;
31
+ };
32
+
33
+ return (
34
+ <div className={ componentClassName } data-testid="pricing-slider">
35
+ <ReactSlider
36
+ className="jp-components-pricing-slider__control"
37
+ thumbClassName="jp-components-pricing-slider__thumb"
38
+ thumbActiveClassName="jp-components-pricing-slider__thumb--is-active"
39
+ trackClassName="jp-components-pricing-slider__track"
40
+ value={ value }
41
+ max={ maxValue }
42
+ min={ minValue }
43
+ step={ step }
44
+ renderThumb={ renderThumbCallback } // eslint-disable-line react/jsx-no-bind
45
+ onChange={ onChange } // eslint-disable-line react/jsx-no-bind
46
+ onBeforeChange={ onBeforeChange } // eslint-disable-line react/jsx-no-bind
47
+ onAfterChange={ onAfterChange } // eslint-disable-line react/jsx-no-bind
48
+ />
49
+ </div>
50
+ );
51
+ };
52
+
53
+ export default PricingSlider;
@@ -0,0 +1,44 @@
1
+ import { useState } from 'react';
2
+ import PricingSlider from '../index';
3
+ import type { ComponentStory, ComponentMeta } from '@storybook/react';
4
+
5
+ export default {
6
+ title: 'JS Packages/Components/Pricing Slider',
7
+ component: PricingSlider,
8
+ } as ComponentMeta< typeof PricingSlider >;
9
+
10
+ // Export additional stories using pre-defined values
11
+ const Template: ComponentStory< typeof PricingSlider > = args => <PricingSlider { ...args } />;
12
+
13
+ // Export Default story
14
+ export const _default = Template.bind( {} );
15
+
16
+ // Export additional stories using chaning values
17
+ const TemplateWithChangingValue: ComponentStory< typeof PricingSlider > = args => {
18
+ const [ value, setValue ] = useState( 10 );
19
+ const [ endValue, setEndValue ] = useState( 10 );
20
+ const renderThumb = ( props, state ) => {
21
+ return (
22
+ <div { ...props }>
23
+ { state.valueNow } - { state.valueNow % 2 === 0 ? 'Even' : 'Odd' }
24
+ </div>
25
+ );
26
+ };
27
+
28
+ return (
29
+ <div>
30
+ <PricingSlider
31
+ { ...args }
32
+ value={ value }
33
+ onChange={ setValue }
34
+ onAfterChange={ setEndValue }
35
+ renderThumb={ renderThumb } // eslint-disable-line react/jsx-no-bind
36
+ />
37
+ <div>{ `Value on changing: ${ value }` }</div>
38
+ <div>{ `Value on change ends: ${ endValue }` }</div>
39
+ </div>
40
+ );
41
+ };
42
+
43
+ // Export With Default Value story
44
+ export const WithDefaultValue = TemplateWithChangingValue.bind( {} );
@@ -0,0 +1,55 @@
1
+ @import '@automattic/jetpack-base-styles/style';
2
+
3
+ $thumb-height: 40px;
4
+ $track-height: 8px;
5
+
6
+ @mixin adjust-track-rail-styles {
7
+ height: $track-height;
8
+ border-radius: 8px; /* stylelint-disable-line scales/radii */
9
+ }
10
+
11
+ // Base styles
12
+ .jp-components-pricing-slider__control {
13
+ width: 100%;
14
+ height: $thumb-height;
15
+ }
16
+
17
+ .jp-components-pricing-slider__track {
18
+ @include adjust-track-rail-styles;
19
+ top: calc(($thumb-height / 2) - ($track-height / 2));
20
+ background: var(--jp-gray);
21
+
22
+ &.jp-components-pricing-slider__track-0 {
23
+ background: var(--jp-green-40);
24
+ }
25
+
26
+ &.jp-components-pricing-slider__track-1 {
27
+ background: var(--jp-gray);
28
+ }
29
+ }
30
+
31
+ .jp-components-pricing-slider__thumb {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ padding: 8px 16px;
36
+ height: $thumb-height;
37
+ background-color: var(--jp-white);
38
+ border: 1px solid var(--jp-gray);
39
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 6px 8px rgba(0, 0, 0, 0.08);
40
+ border-radius: 4px;
41
+ color: var(--jp-black);
42
+ font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif;
43
+ font-style: normal;
44
+ font-weight: 600;
45
+ font-size: var(--font-body);
46
+ line-height: 24px;
47
+ letter-spacing: -0.02em;
48
+ white-space: nowrap;
49
+ cursor: pointer;
50
+
51
+ &.jp-components-pricing-slider__thumb--is-active {
52
+ // On focus styling
53
+ outline: none;
54
+ }
55
+ }
@@ -0,0 +1,14 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import ResizeObserver from 'resize-observer-polyfill';
3
+ import PricingSlider from '../index';
4
+
5
+ describe( 'PricingSlider', () => {
6
+ beforeAll( () => {
7
+ global.ResizeObserver = ResizeObserver;
8
+ } );
9
+
10
+ it( 'renders the pricing slider', () => {
11
+ render( <PricingSlider /> );
12
+ expect( screen.getByTestId( 'pricing-slider' ) ).toBeInTheDocument();
13
+ } );
14
+ } );
@@ -0,0 +1,58 @@
1
+ import type { HTMLProps, RefCallback } from 'react';
2
+
3
+ interface HTMLPropsWithRefCallback< T > extends HTMLProps< T > {
4
+ ref: RefCallback< T >;
5
+ }
6
+
7
+ export type PricingSliderProps = {
8
+ /**
9
+ * The wrapper class name of this PricingSlider component.
10
+ */
11
+ className?: string;
12
+
13
+ /**
14
+ * The maximum value of the slider.
15
+ */
16
+ maxValue?: number;
17
+
18
+ /**
19
+ * The minimum value of the slider.
20
+ */
21
+ minValue?: number;
22
+
23
+ /**
24
+ * The initial value of the slider.
25
+ */
26
+ value?: number;
27
+
28
+ /**
29
+ * The step value of the slider.
30
+ */
31
+ step?: number;
32
+
33
+ /**
34
+ * Callback called on every value change.
35
+ * The function will be called with two arguments, the first being the new value(s) the second being thumb index.
36
+ */
37
+ onChange?: ( value: number ) => void;
38
+
39
+ /**
40
+ * Callback called before starting to move a thumb. The callback will only be called if the action will result in a change.
41
+ * The function will be called with two arguments, the first being the initial value(s) the second being thumb index.
42
+ */
43
+ onBeforeChange?: ( value: number ) => void;
44
+
45
+ /**
46
+ * Callback called only after moving a thumb has ended. The callback will only be called if the action resulted in a change.
47
+ * The function will be called with two arguments, the first being the result value(s) the second being thumb index.
48
+ */
49
+ onAfterChange?: ( value: number ) => void;
50
+
51
+ /**
52
+ * Node to render on the slider.
53
+ */
54
+ renderThumb?: (
55
+ props: HTMLPropsWithRefCallback< HTMLDivElement >,
56
+ state: { index: number; value: number | ReadonlyArray< number >; valueNow: number }
57
+ ) => JSX.Element | null;
58
+ };
@@ -0,0 +1,200 @@
1
+ import { __, sprintf } from '@wordpress/i18n';
2
+ import { Icon, check, closeSmall } from '@wordpress/icons';
3
+ import classnames from 'classnames';
4
+ import {
5
+ createContext,
6
+ useContext,
7
+ Children,
8
+ cloneElement,
9
+ PropsWithChildren,
10
+ ReactElement,
11
+ } from 'react';
12
+ import React, { CSSProperties } from 'react';
13
+ import IconTooltip from '../icon-tooltip';
14
+ import useBreakpointMatch from '../layout/use-breakpoint-match';
15
+ import TermsOfService from '../terms-of-service';
16
+ import Text from '../text';
17
+ import styles from './styles.module.scss';
18
+ import {
19
+ PricingTableProps,
20
+ PricingTableColumnProps,
21
+ PricingTableHeaderProps,
22
+ PricingTableItemProps,
23
+ } from './types';
24
+
25
+ const INCLUDED_TEXT = __( 'Included', 'jetpack' );
26
+ const NOT_INCLUDED_TEXT = __( 'Not included', 'jetpack' );
27
+ const COMING_SOON_TEXT = __( 'Coming soon', 'jetpack' );
28
+
29
+ const PricingTableContext = createContext( undefined );
30
+
31
+ const getItemLabels = ( isComingSoon, isIncluded, featureNameLabel ) => {
32
+ if ( isComingSoon ) {
33
+ return {
34
+ lg: COMING_SOON_TEXT,
35
+ // translators: Name of the current feature
36
+ default: sprintf( __( '%s coming soon', 'jetpack' ), featureNameLabel ),
37
+ };
38
+ }
39
+
40
+ return {
41
+ lg: isIncluded ? INCLUDED_TEXT : NOT_INCLUDED_TEXT,
42
+ default: isIncluded
43
+ ? featureNameLabel
44
+ : sprintf(
45
+ /* translators: Name of the current feature */
46
+ __( '%s not included', 'jetpack' ),
47
+ featureNameLabel
48
+ ),
49
+ };
50
+ };
51
+
52
+ export const PricingTableItem: React.FC< PricingTableItemProps > = ( {
53
+ isIncluded = false,
54
+ isComingSoon = false,
55
+ index = 0,
56
+ label = null,
57
+ tooltipInfo,
58
+ tooltipTitle,
59
+ tooltipClassName = '',
60
+ } ) => {
61
+ const [ isLg ] = useBreakpointMatch( 'lg' );
62
+ const item = useContext( PricingTableContext )[ index ];
63
+ const showTick = isComingSoon || isIncluded;
64
+
65
+ const featureNameLabel = item.name;
66
+
67
+ const defaultTooltipInfo = item.tooltipInfo;
68
+ const defaultTooltipTitle = item.tooltipTitle;
69
+ const showTooltip = tooltipInfo || ( ! isLg && defaultTooltipInfo );
70
+
71
+ const labels = getItemLabels( isComingSoon, isIncluded, featureNameLabel );
72
+
73
+ const defaultLabel = isLg ? labels.lg : labels.default;
74
+
75
+ return (
76
+ <div className={ classnames( styles.item, styles.value ) }>
77
+ <Icon
78
+ className={ classnames(
79
+ styles.icon,
80
+ showTick ? styles[ 'icon-check' ] : styles[ 'icon-cross' ]
81
+ ) }
82
+ size={ 32 }
83
+ icon={ showTick ? check : closeSmall }
84
+ />
85
+ <Text variant="body-small">{ label || defaultLabel }</Text>
86
+ { showTooltip && (
87
+ <IconTooltip
88
+ title={ tooltipTitle ? tooltipTitle : defaultTooltipTitle }
89
+ iconClassName={ styles[ 'popover-icon' ] }
90
+ className={ classnames( styles.popover, tooltipClassName ) }
91
+ placement={ 'bottom-end' }
92
+ iconSize={ 14 }
93
+ offset={ 4 }
94
+ wide={ Boolean( tooltipTitle && tooltipInfo ) }
95
+ >
96
+ <Text variant="body-small" component="div">
97
+ { tooltipInfo || defaultTooltipInfo }
98
+ </Text>
99
+ </IconTooltip>
100
+ ) }
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export const PricingTableHeader: React.FC< PricingTableHeaderProps > = ( { children } ) => (
106
+ <div className={ styles.header }>{ children }</div>
107
+ );
108
+
109
+ export const PricingTableColumn: React.FC< PricingTableColumnProps > = ( {
110
+ primary = false,
111
+ children,
112
+ } ) => {
113
+ let index = 0;
114
+
115
+ return (
116
+ <div className={ classnames( styles.card, { [ styles[ 'is-primary' ] ]: primary } ) }>
117
+ { Children.map( children, child => {
118
+ const item = child as ReactElement<
119
+ PropsWithChildren< PricingTableHeaderProps | PricingTableItemProps >
120
+ >;
121
+
122
+ if ( item.type === PricingTableItem ) {
123
+ index++;
124
+ return cloneElement( item, { index: index - 1 } );
125
+ }
126
+
127
+ return item;
128
+ } ) }
129
+ </div>
130
+ );
131
+ };
132
+
133
+ const PricingTable: React.FC< PricingTableProps > = ( {
134
+ title,
135
+ items,
136
+ children,
137
+ showIntroOfferDisclaimer = false,
138
+ } ) => {
139
+ const [ isLg ] = useBreakpointMatch( 'lg' );
140
+
141
+ return (
142
+ <PricingTableContext.Provider value={ items }>
143
+ <div
144
+ className={ classnames( styles.container, { [ styles[ 'is-viewport-large' ] ]: isLg } ) }
145
+ style={
146
+ {
147
+ '--rows': items.length + 1,
148
+ '--columns': Children.toArray( children ).length + 1,
149
+ } as CSSProperties
150
+ }
151
+ >
152
+ <div className={ styles.table }>
153
+ <Text variant="headline-small">{ title }</Text>
154
+ { isLg &&
155
+ items.map( ( item, i ) => (
156
+ <div
157
+ className={ classnames( styles.item, {
158
+ [ styles[ 'last-feature' ] ]: i === items.length - 1,
159
+ } ) }
160
+ key={ i }
161
+ >
162
+ <Text variant="body-small">
163
+ <strong>{ item.name }</strong>
164
+ </Text>
165
+ { item.tooltipInfo && (
166
+ <IconTooltip
167
+ title={ item.tooltipTitle }
168
+ iconClassName={ styles[ 'popover-icon' ] }
169
+ className={ styles.popover }
170
+ placement={ item.tooltipPlacement ? item.tooltipPlacement : 'bottom-end' }
171
+ iconSize={ 14 }
172
+ offset={ 4 }
173
+ wide={ Boolean( item.tooltipTitle && item.tooltipInfo ) }
174
+ >
175
+ <Text variant="body-small">{ item.tooltipInfo }</Text>
176
+ </IconTooltip>
177
+ ) }
178
+ </div>
179
+ ) ) }
180
+ { children }
181
+ </div>
182
+ </div>
183
+ <div className={ styles[ 'tos-container' ] }>
184
+ <div className={ styles.tos }>
185
+ { showIntroOfferDisclaimer && (
186
+ <Text variant="body-small">
187
+ { __(
188
+ 'Reduced pricing is a limited offer for the first year and renews at regular price.',
189
+ 'jetpack'
190
+ ) }
191
+ </Text>
192
+ ) }
193
+ <TermsOfService multipleButtons />
194
+ </div>
195
+ </div>
196
+ </PricingTableContext.Provider>
197
+ );
198
+ };
199
+
200
+ export default PricingTable;
@@ -0,0 +1,74 @@
1
+ import Button from '../../button';
2
+ import ProductPrice from '../../product-price';
3
+ import PricingTable, { PricingTableColumn, PricingTableHeader, PricingTableItem } from '../index';
4
+ import type { ComponentStory, ComponentMeta } from '@storybook/react';
5
+
6
+ export default {
7
+ title: 'JS Packages/Components/Pricing Table',
8
+ component: PricingTable,
9
+ subcomponents: { PricingTableColumn, PricingTableHeader, PricingTableItem },
10
+ } as ComponentMeta< typeof PricingTable >;
11
+
12
+ const Template: ComponentStory< typeof PricingTable > = args => {
13
+ return (
14
+ <PricingTable { ...args }>
15
+ <PricingTableColumn primary>
16
+ <PricingTableHeader>
17
+ <ProductPrice
18
+ price={ 9.95 }
19
+ offPrice={ 4.98 }
20
+ legend="/month, billed yearly"
21
+ currency="USD"
22
+ hideDiscountLabel={ args?.hideDiscountLabel }
23
+ />
24
+ <Button fullWidth>Get Premium</Button>
25
+ </PricingTableHeader>
26
+ <PricingTableItem isIncluded={ true } label={ <strong>Up to 1000</strong> } />
27
+ <PricingTableItem isIncluded={ true } tooltipInfo={ 'This is an info' } />
28
+ <PricingTableItem
29
+ isIncluded={ true }
30
+ tooltipInfo={ 'This is an info with title' }
31
+ tooltipTitle={ 'Small title' }
32
+ />
33
+ <PricingTableItem isIncluded={ true } />
34
+ <PricingTableItem isIncluded={ true } />
35
+ </PricingTableColumn>
36
+ <PricingTableColumn>
37
+ <PricingTableHeader>
38
+ <ProductPrice price={ 0 } legend="Free forever" currency="USD" hidePriceFraction />
39
+ <Button fullWidth variant="secondary">
40
+ Start for free
41
+ </Button>
42
+ </PricingTableHeader>
43
+ <PricingTableItem isIncluded={ true } label="Up to 300" />
44
+ <PricingTableItem
45
+ isIncluded={ false }
46
+ label="This is not included"
47
+ tooltipInfo="This has a tooltip, so its overwrites the default info on small screens"
48
+ />
49
+ <PricingTableItem isIncluded={ false } />
50
+ <PricingTableItem isIncluded={ true } />
51
+ <PricingTableItem isIncluded={ true } />
52
+ </PricingTableColumn>
53
+ </PricingTable>
54
+ );
55
+ };
56
+
57
+ const DefaultArgs = {
58
+ title: 'Buy premium, or start for free',
59
+ items: [
60
+ { name: 'Feature A with limit', tooltipInfo: 'Default info for Feature A' },
61
+ { name: 'Feature B', tooltipInfo: 'Default info for Feature B' },
62
+ {
63
+ name: 'Feature C with a longer title that will span multiple lines',
64
+ tooltipInfo: 'Default info for Feature C',
65
+ tooltipTitle: 'Title for C',
66
+ },
67
+ { name: 'Feature D', tooltipInfo: 'Default info for Feature D', tooltipTitle: 'Title for D' },
68
+ { name: 'Feature E' },
69
+ ],
70
+ hideDiscountLabel: false,
71
+ };
72
+
73
+ export const _default = Template.bind( {} );
74
+ _default.args = DefaultArgs;
@@ -0,0 +1,160 @@
1
+ .container {
2
+ --padding: calc(var(--spacing-base) * 4);
3
+ color: var(--jp-black);
4
+ }
5
+
6
+ .table {
7
+ --gap: calc(var(--spacing-base) * 3);
8
+ position: relative;
9
+ padding: var(--padding) 0;
10
+
11
+ .is-viewport-large & {
12
+ display: grid;
13
+ grid-template-columns: repeat(var(--columns), 1fr);
14
+ grid-auto-flow: column;
15
+ grid-template-rows: repeat(var(--rows), minmax(min-content, max-content));
16
+ column-gap: var(--gap);
17
+ }
18
+ }
19
+
20
+ .card {
21
+ margin-top: var(--padding);
22
+
23
+ .is-viewport-large & {
24
+ display: contents;
25
+ }
26
+
27
+ &.is-primary {
28
+ > * {
29
+ background: var(--jp-white);
30
+ position: relative;
31
+
32
+ &::after {
33
+ content: '';
34
+ position: absolute;
35
+ top: 0;
36
+ left: 0;
37
+ right: 0;
38
+ bottom: 0;
39
+ z-index: -1;
40
+ box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.05);
41
+ }
42
+ }
43
+ }
44
+
45
+ > :first-child {
46
+ border-top-left-radius: var(--jp-border-radius);
47
+ border-top-right-radius: var(--jp-border-radius);
48
+ border-width: 1px 1px 0;
49
+ }
50
+
51
+ > :last-child {
52
+ border-width: 0 1px 1px;
53
+ border-bottom-left-radius: var(--jp-border-radius);
54
+ border-bottom-right-radius: var(--jp-border-radius);
55
+ }
56
+ }
57
+
58
+ .header {
59
+ padding: var(--padding);
60
+ display: flex;
61
+ flex-direction: column;
62
+ justify-content: space-between;
63
+ }
64
+
65
+ .item {
66
+ display: flex;
67
+ align-items: center;
68
+ padding-bottom: calc(var(--spacing-base) * 2);
69
+ position: relative;
70
+
71
+ &:not(:nth-child(2)) {
72
+ padding-top: calc(var(--spacing-base) * 2);
73
+
74
+ &::before {
75
+ content: '';
76
+ position: absolute;
77
+ top: 0;
78
+ left: var(--padding);
79
+ right: var(--padding);
80
+ height: 1px;
81
+ .is-viewport-large & {
82
+ width: calc(100% + var(--gap));
83
+ left: 0;
84
+ right: unset;
85
+ }
86
+ z-index: 5;
87
+ background-color: var(--jp-gray);
88
+
89
+ // Last pricing column doesn't have any grid gap on the right. So override the default width specified above.
90
+ .is-viewport-large .table > :last-child & {
91
+ width: 100%;
92
+ }
93
+ }
94
+ }
95
+
96
+ &:last-of-type {
97
+ padding-bottom: var(--padding);
98
+ }
99
+ }
100
+
101
+ .last-feature {
102
+ padding-bottom: var(--padding);
103
+ }
104
+
105
+ .value {
106
+ padding-left: var(--padding);
107
+ padding-right: var(--padding);
108
+ }
109
+
110
+ .icon {
111
+ margin: 0 var(--spacing-base);
112
+ fill: var(--jp-gray);
113
+ flex-shrink: 0;
114
+
115
+ &.icon-check {
116
+ fill: var(--jp-green-40);
117
+ }
118
+
119
+ &.icon-cross {
120
+ fill: var(--jp-red-50);
121
+ }
122
+ }
123
+
124
+ .popover {
125
+ margin-left: auto;
126
+
127
+ .is-viewport-large & {
128
+ top: 1px;
129
+ margin: 0 var(--spacing-base);
130
+ }
131
+ }
132
+
133
+ .item .popover-icon {
134
+ fill: var(--jp-gray-20);
135
+ flex-shrink: 0;
136
+ }
137
+
138
+ .tos {
139
+ text-align: center;
140
+ width: fit-content;
141
+
142
+ > a {
143
+ color: black;
144
+ }
145
+
146
+ .is-viewport-large & {
147
+ padding-left: var(--padding);
148
+ padding-right: var(--padding);
149
+ grid-column: 2;
150
+ white-space: nowrap;
151
+ overflow: hidden;
152
+ }
153
+ }
154
+
155
+ .tos-container {
156
+ display: flex;
157
+ align-items: right;
158
+ justify-content: right;
159
+ margin: 0 calc(var(--spacing-base) * 4);
160
+ }
@@ -0,0 +1,41 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import PricingTable, { PricingTableColumn, PricingTableHeader, PricingTableItem } from '../index';
3
+
4
+ describe( 'PricingTable', () => {
5
+ const testProps = {
6
+ title: 'Dummy Pricing Table',
7
+ items: [
8
+ { name: 'Dummy Item 1', tooltipInfo: 'Default Info' },
9
+ { name: 'Dummy Item 2', tooltipInfo: 'Default Info' },
10
+ { name: 'Dummy Item 3', tooltipInfo: 'Default Info' },
11
+ ],
12
+ children: (
13
+ <>
14
+ <PricingTableColumn>
15
+ <PricingTableHeader>Header 1</PricingTableHeader>
16
+ <PricingTableItem isIncluded={ true } />
17
+ <PricingTableItem isIncluded={ true } />
18
+ <PricingTableItem isIncluded={ true } />
19
+ </PricingTableColumn>
20
+ <PricingTableColumn>
21
+ <PricingTableHeader>Header 2</PricingTableHeader>
22
+ <PricingTableItem isIncluded={ true } />
23
+ <PricingTableItem isIncluded={ true } />
24
+ <PricingTableItem isIncluded={ false } />
25
+ </PricingTableColumn>
26
+ </>
27
+ ),
28
+ };
29
+
30
+ it( 'renders the title', () => {
31
+ render( <PricingTable { ...testProps }></PricingTable> );
32
+ expect( screen.getByRole( 'heading' ) ).toHaveTextContent( 'Dummy Pricing Table' );
33
+ } );
34
+
35
+ it( 'renders all included items', () => {
36
+ render( <PricingTable { ...testProps }></PricingTable> );
37
+ expect( screen.getAllByText( 'Dummy Item 1' ) ).toHaveLength( 2 );
38
+ expect( screen.getAllByText( 'Dummy Item 2' ) ).toHaveLength( 2 );
39
+ expect( screen.getAllByText( 'Dummy Item 3' ) ).toHaveLength( 1 ); // eslint-disable-line jest-dom/prefer-in-document
40
+ } );
41
+ } );