@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.
- package/.gitattributes +8 -0
- package/CHANGELOG.md +747 -0
- package/LICENSE.txt +357 -0
- package/README.md +11 -0
- package/SECURITY.md +38 -0
- package/components/action-button/index.jsx +70 -0
- package/components/action-button/stories/index.stories.jsx +38 -0
- package/components/action-button/style.module.scss +11 -0
- package/components/action-button/test/component.jsx +16 -0
- package/components/action-popover/index.tsx +105 -0
- package/components/action-popover/stories/index.stories.jsx +115 -0
- package/components/action-popover/stories/styles.module.scss +7 -0
- package/components/action-popover/styles.module.scss +49 -0
- package/components/action-popover/types.ts +20 -0
- package/components/admin-page/index.tsx +58 -0
- package/components/admin-page/stories/index.stories.tsx +35 -0
- package/components/admin-page/stories/style.module.scss +13 -0
- package/components/admin-page/style.module.scss +11 -0
- package/components/admin-page/types.ts +41 -0
- package/components/admin-section/basic/index.tsx +15 -0
- package/components/admin-section/basic/style.module.scss +3 -0
- package/components/admin-section/hero/index.tsx +15 -0
- package/components/admin-section/hero/style.module.scss +3 -0
- package/components/admin-section/stories/index.stories.tsx +60 -0
- package/components/admin-section/types.ts +6 -0
- package/components/alert/index.tsx +61 -0
- package/components/alert/stories/index.stories.jsx +24 -0
- package/components/alert/style.module.scss +43 -0
- package/components/automattic-byline-logo/README.md +16 -0
- package/components/automattic-byline-logo/index.tsx +38 -0
- package/components/automattic-byline-logo/stories/index.stories.tsx +20 -0
- package/components/automattic-byline-logo/test/component.tsx +28 -0
- package/components/automattic-byline-logo/types.ts +18 -0
- package/components/boost-score-bar/index.tsx +103 -0
- package/components/boost-score-bar/stories/index.stories.tsx +39 -0
- package/components/boost-score-bar/style.scss +184 -0
- package/components/boost-score-bar/test/component.tsx +102 -0
- package/components/boost-score-bar/types.ts +9 -0
- package/components/button/index.tsx +84 -0
- package/components/button/stories/index.mdx +90 -0
- package/components/button/stories/index.stories.tsx +400 -0
- package/components/button/stories/style.module.scss +20 -0
- package/components/button/style.module.scss +212 -0
- package/components/button/types.ts +21 -0
- package/components/contextual-upgrade-trigger/README.md +81 -0
- package/components/contextual-upgrade-trigger/index.tsx +44 -0
- package/components/contextual-upgrade-trigger/stories/index.stories.tsx +25 -0
- package/components/contextual-upgrade-trigger/style.module.scss +75 -0
- package/components/contextual-upgrade-trigger/types.ts +9 -0
- package/components/decorative-card/index.tsx +44 -0
- package/components/decorative-card/stories/index.stories.tsx +18 -0
- package/components/decorative-card/style.scss +116 -0
- package/components/decorative-card/types.ts +16 -0
- package/components/dialog/index.tsx +69 -0
- package/components/dialog/stories/boost.png +0 -0
- package/components/dialog/stories/declaration.d.ts +1 -0
- package/components/dialog/stories/index.stories.tsx +118 -0
- package/components/dialog/stories/js-components.components.dialog.mdx +62 -0
- package/components/dialog/stories/js-components.components.dialog.stories.js +37 -0
- package/components/dialog/stories/style.module.scss +21 -0
- package/components/dialog/style.module.scss +30 -0
- package/components/donut-meter/index.tsx +147 -0
- package/components/donut-meter/stories/index.stories.tsx +51 -0
- package/components/donut-meter/style.scss +11 -0
- package/components/donut-meter/test/component.tsx +24 -0
- package/components/gridicon/index.tsx +298 -0
- package/components/gridicon/stories/index.stories.tsx +18 -0
- package/components/gridicon/style.scss +16 -0
- package/components/gridicon/types.ts +48 -0
- package/components/icon-tooltip/index.tsx +96 -0
- package/components/icon-tooltip/stories/index.stories.tsx +108 -0
- package/components/icon-tooltip/stories/style.scss +36 -0
- package/components/icon-tooltip/style.scss +121 -0
- package/components/icon-tooltip/test/component.tsx +15 -0
- package/components/icon-tooltip/types.ts +76 -0
- package/components/icons/README.md +98 -0
- package/components/icons/index.tsx +369 -0
- package/components/icons/stories/index.stories.tsx +65 -0
- package/components/icons/stories/style.module.scss +33 -0
- package/components/icons/style.module.scss +40 -0
- package/components/icons/types.ts +38 -0
- package/components/indeterminate-progress-bar/index.tsx +28 -0
- package/components/indeterminate-progress-bar/stories/index.stories.tsx +14 -0
- package/components/indeterminate-progress-bar/style.module.scss +34 -0
- package/components/indeterminate-progress-bar/types.ts +6 -0
- package/components/jetpack-footer/README.md +21 -0
- package/components/jetpack-footer/index.tsx +94 -0
- package/components/jetpack-footer/stories/index.stories.tsx +40 -0
- package/components/jetpack-footer/style.scss +116 -0
- package/components/jetpack-footer/test/component.tsx +45 -0
- package/components/jetpack-footer/types.ts +36 -0
- package/components/jetpack-logo/README.md +20 -0
- package/components/jetpack-logo/index.tsx +46 -0
- package/components/jetpack-logo/stories/index.stories.tsx +20 -0
- package/components/jetpack-logo/test/component.tsx +16 -0
- package/components/jetpack-logo/types.ts +7 -0
- package/components/jetpack-search-logo/README.md +20 -0
- package/components/jetpack-search-logo/index.tsx +93 -0
- package/components/jetpack-search-logo/stories/index.stories.tsx +21 -0
- package/components/jetpack-search-logo/test/component.tsx +16 -0
- package/components/jetpack-search-logo/types.ts +7 -0
- package/components/jetpack-vaultpress-backup-logo/README.md +19 -0
- package/components/jetpack-vaultpress-backup-logo/index.tsx +113 -0
- package/components/jetpack-vaultpress-backup-logo/stories/index.stories.tsx +20 -0
- package/components/jetpack-vaultpress-backup-logo/test/component.tsx +18 -0
- package/components/jetpack-vaultpress-backup-logo/types.ts +6 -0
- package/components/jetpack-videopress-logo/README.md +19 -0
- package/components/jetpack-videopress-logo/index.tsx +93 -0
- package/components/jetpack-videopress-logo/stories/index.stories.tsx +20 -0
- package/components/jetpack-videopress-logo/test/component.tsx +16 -0
- package/components/jetpack-videopress-logo/types.tsx +6 -0
- package/components/layout/README.md +9 -0
- package/components/layout/breakpoints.module.scss +20 -0
- package/components/layout/col/README.md +81 -0
- package/components/layout/col/index.tsx +49 -0
- package/components/layout/col/style.module.scss +28 -0
- package/components/layout/container/README.md +77 -0
- package/components/layout/container/index.tsx +42 -0
- package/components/layout/container/style.module.scss +35 -0
- package/components/layout/stories/index.stories.tsx +145 -0
- package/components/layout/stories/styles.module.scss +22 -0
- package/components/layout/types.ts +55 -0
- package/components/layout/use-breakpoint-match/README.md +43 -0
- package/components/layout/use-breakpoint-match/declaration.d.ts +9 -0
- package/components/layout/use-breakpoint-match/index.ts +79 -0
- package/components/loading-placeholder/README.md +22 -0
- package/components/loading-placeholder/index.tsx +23 -0
- package/components/loading-placeholder/stories/index.stories.tsx +78 -0
- package/components/loading-placeholder/style.module.scss +18 -0
- package/components/notice/index.tsx +98 -0
- package/components/notice/stories/index.stories.jsx +90 -0
- package/components/notice/style.module.scss +107 -0
- package/components/number-format/README.md +25 -0
- package/components/number-format/index.ts +16 -0
- package/components/pricing-card/index.tsx +127 -0
- package/components/pricing-card/stories/index.stories.tsx +55 -0
- package/components/pricing-card/style.scss +112 -0
- package/components/pricing-card/test/component.tsx +89 -0
- package/components/pricing-card/types.ts +52 -0
- package/components/pricing-slider/index.tsx +53 -0
- package/components/pricing-slider/stories/index.stories.tsx +44 -0
- package/components/pricing-slider/style.scss +55 -0
- package/components/pricing-slider/test/component.tsx +14 -0
- package/components/pricing-slider/types.ts +58 -0
- package/components/pricing-table/index.tsx +200 -0
- package/components/pricing-table/stories/index.stories.tsx +74 -0
- package/components/pricing-table/styles.module.scss +160 -0
- package/components/pricing-table/test/component.tsx +41 -0
- package/components/pricing-table/types.ts +84 -0
- package/components/product-offer/icons-card.tsx +47 -0
- package/components/product-offer/index.tsx +115 -0
- package/components/product-offer/product-offer-header.tsx +23 -0
- package/components/product-offer/stories/index.stories.tsx +104 -0
- package/components/product-offer/style.module.scss +93 -0
- package/components/product-offer/types.ts +125 -0
- package/components/product-price/index.tsx +90 -0
- package/components/product-price/price.tsx +35 -0
- package/components/product-price/stories/index.stories.tsx +33 -0
- package/components/product-price/style.module.scss +69 -0
- package/components/product-price/test/component.tsx +26 -0
- package/components/product-price/types.ts +73 -0
- package/components/qr-code/Readme.md +5 -0
- package/components/qr-code/index.tsx +78 -0
- package/components/qr-code/stories/index.stories.tsx +32 -0
- package/components/record-meter-bar/index.tsx +135 -0
- package/components/record-meter-bar/stories/index.stories.tsx +39 -0
- package/components/record-meter-bar/style.scss +38 -0
- package/components/record-meter-bar/test/component.tsx +52 -0
- package/components/spinner/README.md +28 -0
- package/components/spinner/index.jsx +45 -0
- package/components/spinner/stories/index.stories.jsx +20 -0
- package/components/spinner/style.scss +31 -0
- package/components/spinner/test/component.jsx +17 -0
- package/components/split-button/index.tsx +39 -0
- package/components/split-button/stories/index.stories.tsx +35 -0
- package/components/split-button/style.module.scss +21 -0
- package/components/split-button/types.ts +14 -0
- package/components/stat-card/index.tsx +49 -0
- package/components/stat-card/stories/index.mdx +35 -0
- package/components/stat-card/stories/index.stories.tsx +41 -0
- package/components/stat-card/style.module.scss +49 -0
- package/components/stat-card/test/component.tsx +24 -0
- package/components/stat-card/types.ts +28 -0
- package/components/status/README.md +42 -0
- package/components/status/index.tsx +40 -0
- package/components/status/stories/index.stories.tsx +44 -0
- package/components/status/style.module.scss +72 -0
- package/components/terms-of-service/index.tsx +63 -0
- package/components/terms-of-service/styles.scss +10 -0
- package/components/terms-of-service/test/index.test.jsx +52 -0
- package/components/terms-of-service/types.ts +30 -0
- package/components/testimonials/index.tsx +58 -0
- package/components/testimonials/style.scss +127 -0
- package/components/testimonials/test/component.tsx +103 -0
- package/components/testimonials/testimonial.tsx +47 -0
- package/components/testimonials/types.ts +17 -0
- package/components/text/constants.ts +30 -0
- package/components/text/index.tsx +91 -0
- package/components/text/stories/headings.index.stories.tsx +80 -0
- package/components/text/stories/index.mdx +195 -0
- package/components/text/stories/index.stories.tsx +69 -0
- package/components/text/stories/style.module.scss +64 -0
- package/components/text/stories/title.index.stories.tsx +34 -0
- package/components/text/style.module.scss +125 -0
- package/components/text/types.ts +53 -0
- package/components/theme-provider/Readme.md +49 -0
- package/components/theme-provider/globals.module.scss +6 -0
- package/components/theme-provider/index.tsx +151 -0
- package/components/theme-provider/stories/index.stories.tsx +164 -0
- package/components/theme-provider/stories/style.module.scss +79 -0
- package/components/theme-provider/types.ts +25 -0
- package/components/toggle-control/README.md +5 -0
- package/components/toggle-control/index.tsx +67 -0
- package/components/toggle-control/stories/index.stories.tsx +83 -0
- package/components/toggle-control/styles.module.scss +41 -0
- package/components/zendesk-chat/README.md +26 -0
- package/components/zendesk-chat/constants.ts +2 -0
- package/components/zendesk-chat/index.tsx +22 -0
- package/components/zendesk-chat/stories/index.stories.tsx +15 -0
- package/components/zendesk-chat/test/component.tsx +16 -0
- package/components/zendesk-chat/types.ts +3 -0
- package/index.ts +69 -0
- package/lib/locale/index.ts +48 -0
- package/lib/locale/test/index.ts +21 -0
- package/package.json +80 -0
- package/tools/get-product-checkout-url/Readme.md +32 -0
- package/tools/get-product-checkout-url/index.ts +31 -0
- package/tools/jp-redirect/README.md +61 -0
- package/tools/jp-redirect/index.ts +62 -0
- package/tools/jp-redirect/test/index.tsx +89 -0
- package/tools/jp-redirect/types.ts +49 -0
- package/tools/pricing-utils/README.md +16 -0
- package/tools/pricing-utils/index.ts +11 -0
- package/tools/pricing-utils/test/index.ts +39 -0
- 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
|
+
} );
|