newspack-components 4.7.0 → 4.8.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/CHANGELOG.md +20 -0
- package/DEVELOPMENT.md +42 -21
- package/dist/cjs/_mixins.scss +13 -0
- package/dist/cjs/_variables.scss +12 -0
- package/dist/cjs/action-card/index.js +18 -10
- package/dist/cjs/action-card/index.test.js +91 -0
- package/dist/cjs/action-card/style.scss +2 -40
- package/dist/cjs/button/index.js +8 -3
- package/dist/cjs/button/index.test.js +61 -5
- package/dist/cjs/card/core-card.js +0 -2
- package/dist/cjs/card/style-core.scss +7 -5
- package/dist/cjs/card-feature/index.js +75 -61
- package/dist/cjs/card-feature/index.test.js +396 -54
- package/dist/cjs/card-feature/style.scss +20 -31
- package/dist/cjs/card-form/index.js +5 -5
- package/dist/cjs/card-sortable-list/index.js +5 -3
- package/dist/cjs/card-sortable-list/index.test.js +50 -0
- package/dist/cjs/collapsible-group/context.js +19 -0
- package/dist/cjs/collapsible-group/index.js +24 -0
- package/dist/cjs/collapsible-group/index.test.js +215 -0
- package/dist/cjs/collapsible-group/item.js +61 -0
- package/dist/cjs/collapsible-group/root.js +62 -0
- package/dist/cjs/collapsible-group/style.scss +47 -0
- package/dist/cjs/collapsible-group/types.js +5 -0
- package/dist/cjs/empty-state/actions.js +45 -0
- package/dist/cjs/empty-state/context.js +41 -0
- package/dist/cjs/empty-state/header.js +66 -0
- package/dist/cjs/empty-state/index.js +31 -0
- package/dist/cjs/empty-state/index.test.js +303 -0
- package/dist/cjs/empty-state/root.js +56 -0
- package/dist/cjs/empty-state/style.scss +51 -0
- package/dist/cjs/empty-state/types.js +5 -0
- package/dist/cjs/index.js +34 -20
- package/dist/cjs/info-button/index.js +59 -43
- package/dist/cjs/info-button/index.test.js +199 -0
- package/dist/cjs/info-button/style.scss +41 -9
- package/dist/cjs/modal/style.scss +12 -12
- package/dist/cjs/newspack-icon/style.scss +4 -4
- package/dist/cjs/notice/style.scss +3 -3
- package/dist/cjs/page/style.scss +16 -16
- package/dist/cjs/plugin-installer/index.js +5 -9
- package/dist/cjs/plugin-installer/style.scss +3 -14
- package/dist/cjs/section-header/index.js +31 -18
- package/dist/cjs/section-header/index.test.js +40 -0
- package/dist/cjs/section-header/style.scss +22 -6
- package/dist/cjs/settings/DateRangeSetting.js +309 -0
- package/dist/cjs/settings/SettingsSection.js +19 -5
- package/dist/cjs/settings/SettingsSection.test.js +49 -0
- package/dist/cjs/settings/index.js +3 -1
- package/dist/cjs/settings/style.scss +40 -7
- package/dist/cjs/stat-card/body.js +41 -0
- package/dist/cjs/stat-card/constants.js +11 -0
- package/dist/cjs/stat-card/context.js +54 -0
- package/dist/cjs/stat-card/delta.js +91 -0
- package/dist/cjs/stat-card/footer.js +72 -0
- package/dist/cjs/stat-card/index.js +47 -0
- package/dist/cjs/stat-card/index.test.js +938 -0
- package/dist/cjs/stat-card/label.js +66 -0
- package/dist/cjs/stat-card/root.js +69 -0
- package/dist/cjs/stat-card/secondary.js +38 -0
- package/dist/cjs/stat-card/style.scss +89 -0
- package/dist/cjs/stat-card/types.js +5 -0
- package/dist/cjs/stat-card/value.js +78 -0
- package/dist/cjs/status-indicator/index.js +66 -0
- package/dist/cjs/status-indicator/index.test.js +101 -0
- package/dist/cjs/status-indicator/statuses.js +39 -0
- package/dist/cjs/status-indicator/style.scss +9 -0
- package/dist/cjs/status-indicator/types.js +5 -0
- package/dist/cjs/style.scss +9 -9
- package/dist/cjs/tabbed-navigation/style.scss +6 -4
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/web-preview/style.scss +11 -11
- package/dist/cjs/with-wizard/style.scss +10 -10
- package/dist/cjs/with-wizard-screen/style.scss +9 -9
- package/dist/cjs/wizard/index.js +33 -4
- package/dist/cjs/wizard/store/index.js +23 -1
- package/dist/esm/_mixins.scss +13 -0
- package/dist/esm/_variables.scss +12 -0
- package/dist/esm/action-card/index.js +18 -10
- package/dist/esm/action-card/index.test.js +89 -0
- package/dist/esm/action-card/style.scss +2 -40
- package/dist/esm/button/index.js +9 -4
- package/dist/esm/button/index.test.js +63 -5
- package/dist/esm/card/core-card.js +0 -2
- package/dist/esm/card/style-core.scss +7 -5
- package/dist/esm/card-feature/index.js +78 -62
- package/dist/esm/card-feature/index.test.js +397 -55
- package/dist/esm/card-feature/style.scss +20 -31
- package/dist/esm/card-form/index.js +5 -5
- package/dist/esm/card-sortable-list/index.js +6 -4
- package/dist/esm/card-sortable-list/index.test.js +46 -0
- package/dist/esm/collapsible-group/context.js +13 -0
- package/dist/esm/collapsible-group/index.js +17 -0
- package/dist/esm/collapsible-group/index.test.js +215 -0
- package/dist/esm/collapsible-group/item.js +53 -0
- package/dist/esm/collapsible-group/root.js +54 -0
- package/dist/esm/collapsible-group/style.scss +47 -0
- package/dist/esm/collapsible-group/types.js +1 -0
- package/dist/esm/empty-state/actions.js +37 -0
- package/dist/esm/empty-state/context.js +35 -0
- package/dist/esm/empty-state/header.js +58 -0
- package/dist/esm/empty-state/index.js +23 -0
- package/dist/esm/empty-state/index.test.js +299 -0
- package/dist/esm/empty-state/root.js +56 -0
- package/dist/esm/empty-state/style.scss +51 -0
- package/dist/esm/empty-state/types.js +1 -0
- package/dist/esm/index.js +4 -2
- package/dist/esm/info-button/index.js +54 -43
- package/dist/esm/info-button/index.test.js +195 -0
- package/dist/esm/info-button/style.scss +41 -9
- package/dist/esm/modal/style.scss +12 -12
- package/dist/esm/newspack-icon/style.scss +4 -4
- package/dist/esm/notice/style.scss +3 -3
- package/dist/esm/page/style.scss +16 -16
- package/dist/esm/plugin-installer/index.js +5 -9
- package/dist/esm/plugin-installer/style.scss +3 -14
- package/dist/esm/section-header/index.js +27 -14
- package/dist/esm/section-header/index.test.js +36 -0
- package/dist/esm/section-header/style.scss +22 -6
- package/dist/esm/settings/DateRangeSetting.js +304 -0
- package/dist/esm/settings/SettingsSection.js +16 -3
- package/dist/esm/settings/SettingsSection.test.js +45 -0
- package/dist/esm/settings/index.js +3 -1
- package/dist/esm/settings/style.scss +40 -7
- package/dist/esm/stat-card/body.js +36 -0
- package/dist/esm/stat-card/constants.js +5 -0
- package/dist/esm/stat-card/context.js +48 -0
- package/dist/esm/stat-card/delta.js +86 -0
- package/dist/esm/stat-card/footer.js +67 -0
- package/dist/esm/stat-card/index.js +33 -0
- package/dist/esm/stat-card/index.test.js +939 -0
- package/dist/esm/stat-card/label.js +61 -0
- package/dist/esm/stat-card/root.js +64 -0
- package/dist/esm/stat-card/secondary.js +33 -0
- package/dist/esm/stat-card/style.scss +89 -0
- package/dist/esm/stat-card/types.js +1 -0
- package/dist/esm/stat-card/value.js +73 -0
- package/dist/esm/status-indicator/index.js +52 -0
- package/dist/esm/status-indicator/index.test.js +96 -0
- package/dist/esm/status-indicator/statuses.js +33 -0
- package/dist/esm/status-indicator/style.scss +9 -0
- package/dist/esm/status-indicator/types.js +1 -0
- package/dist/esm/style.scss +9 -9
- package/dist/esm/tabbed-navigation/style.scss +6 -4
- package/dist/esm/types.js +1 -0
- package/dist/esm/web-preview/style.scss +11 -11
- package/dist/esm/with-wizard/style.scss +10 -10
- package/dist/esm/with-wizard-screen/style.scss +9 -9
- package/dist/esm/wizard/index.js +38 -7
- package/dist/esm/wizard/store/index.js +23 -1
- package/package.json +1 -1
- package/src/_mixins.scss +13 -0
- package/src/_variables.scss +12 -0
- package/src/action-card/action-card.d.ts +3 -2
- package/src/action-card/index.js +12 -12
- package/src/action-card/index.test.js +56 -0
- package/src/action-card/style.scss +2 -40
- package/src/button/index.test.js +52 -5
- package/src/button/index.tsx +9 -4
- package/src/card/core-card.js +0 -2
- package/src/card/style-core.scss +7 -5
- package/src/card-feature/README.md +68 -27
- package/src/card-feature/index.test.js +242 -27
- package/src/card-feature/index.tsx +77 -67
- package/src/card-feature/style.scss +20 -31
- package/src/card-form/README.md +10 -10
- package/src/card-form/index.tsx +4 -8
- package/src/card-sortable-list/README.md +4 -5
- package/src/card-sortable-list/index.test.js +29 -0
- package/src/card-sortable-list/index.tsx +7 -4
- package/src/collapsible-group/README.md +50 -0
- package/src/collapsible-group/context.ts +13 -0
- package/src/collapsible-group/index.test.js +168 -0
- package/src/collapsible-group/index.tsx +17 -0
- package/src/collapsible-group/item.tsx +42 -0
- package/src/collapsible-group/root.tsx +42 -0
- package/src/collapsible-group/style.scss +47 -0
- package/src/collapsible-group/types.ts +22 -0
- package/src/drawer/README.md +2 -2
- package/src/empty-state/README.md +165 -0
- package/src/empty-state/actions.tsx +37 -0
- package/src/empty-state/context.ts +40 -0
- package/src/empty-state/header.tsx +48 -0
- package/src/empty-state/index.test.js +247 -0
- package/src/empty-state/index.tsx +22 -0
- package/src/empty-state/root.tsx +39 -0
- package/src/empty-state/style.scss +51 -0
- package/src/empty-state/types.ts +33 -0
- package/src/index.js +4 -2
- package/src/info-button/README.md +115 -0
- package/src/info-button/index.test.js +163 -0
- package/src/info-button/index.tsx +64 -0
- package/src/info-button/style.scss +41 -9
- package/src/modal/style.scss +12 -12
- package/src/newspack-icon/style.scss +4 -4
- package/src/notice/style.scss +3 -3
- package/src/page/style.scss +16 -16
- package/src/plugin-installer/index.js +3 -9
- package/src/plugin-installer/style.scss +3 -14
- package/src/section-header/index.js +27 -14
- package/src/section-header/index.test.js +28 -0
- package/src/section-header/style.scss +22 -6
- package/src/settings/DateRangeSetting.js +266 -0
- package/src/settings/SettingsSection.js +24 -4
- package/src/settings/SettingsSection.test.js +45 -0
- package/src/settings/index.js +2 -0
- package/src/settings/style.scss +40 -7
- package/src/stat-card/README.md +434 -0
- package/src/stat-card/body.tsx +28 -0
- package/src/stat-card/constants.ts +5 -0
- package/src/stat-card/context.ts +53 -0
- package/src/stat-card/delta.tsx +88 -0
- package/src/stat-card/footer.tsx +68 -0
- package/src/stat-card/index.test.js +818 -0
- package/src/stat-card/index.tsx +48 -0
- package/src/stat-card/label.tsx +61 -0
- package/src/stat-card/root.tsx +40 -0
- package/src/stat-card/secondary.tsx +27 -0
- package/src/stat-card/style.scss +89 -0
- package/src/stat-card/types.ts +84 -0
- package/src/stat-card/value.tsx +72 -0
- package/src/status-indicator/README.md +123 -0
- package/src/status-indicator/index.test.js +70 -0
- package/src/status-indicator/index.tsx +39 -0
- package/src/status-indicator/statuses.ts +35 -0
- package/src/status-indicator/style.scss +9 -0
- package/src/status-indicator/types.ts +28 -0
- package/src/style.scss +9 -9
- package/src/tabbed-navigation/style.scss +6 -4
- package/src/types.ts +25 -0
- package/src/web-preview/style.scss +11 -11
- package/src/with-wizard/style.scss +10 -10
- package/src/with-wizard-screen/style.scss +9 -9
- package/src/wizard/index.js +49 -7
- package/src/wizard/store/index.js +26 -1
- package/dist/cjs/accordion/index.js +0 -72
- package/dist/cjs/accordion/index.test.js +0 -80
- package/dist/cjs/accordion/style.scss +0 -35
- package/dist/cjs/badge/index.js +0 -32
- package/dist/cjs/badge/style.scss +0 -36
- package/dist/esm/accordion/index.js +0 -63
- package/dist/esm/accordion/index.test.js +0 -79
- package/dist/esm/accordion/style.scss +0 -35
- package/dist/esm/badge/index.js +0 -24
- package/dist/esm/badge/style.scss +0 -36
- package/src/accordion/index.js +0 -46
- package/src/accordion/index.test.js +0 -62
- package/src/accordion/style.scss +0 -35
- package/src/badge/index.tsx +0 -26
- package/src/badge/style.scss +0 -36
- package/src/info-button/index.js +0 -38
|
@@ -15,11 +15,11 @@ Cards sit side by side, so a bare "Enable" names no feature out of context. The
|
|
|
15
15
|
|
|
16
16
|
| State | Condition | Button | Dropdown | Badge |
|
|
17
17
|
|---|---|---|---|---|
|
|
18
|
-
| **Unmet requirements** | `requirements` is set | "Enable" —
|
|
18
|
+
| **Unmet requirements** | `requirements` is set | "Enable" — blocked but still focusable, and described by the badge (clickable if `requirementsActionable`) | Shown if `enabled` and `requirementsActionable` (and `moreControls` provided); otherwise hidden | High-intent badge with `requirements` text |
|
|
19
19
|
| **Disabled** | `!enabled`, no requirements | "Enable" | Hidden | None |
|
|
20
|
-
| **Enabled** | `enabled`, no requirements | "Configure" | Shown if `moreControls` provided |
|
|
20
|
+
| **Enabled** | `enabled`, no requirements | "Configure" | Shown if `moreControls` provided | Stable-intent badge ("Enabled") |
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
When `requirements` is set the title drops to the muted text colour. The description already uses that colour in every state, so the unmet-requirements state is signalled by the title colour plus the high-intent badge.
|
|
23
23
|
|
|
24
24
|
## Basic usage
|
|
25
25
|
|
|
@@ -40,7 +40,21 @@ import { __ } from '@wordpress/i18n';
|
|
|
40
40
|
|
|
41
41
|
## With unmet requirements
|
|
42
42
|
|
|
43
|
-
When `requirements` is set the button is
|
|
43
|
+
When `requirements` is set, a high-intent badge displays the string and the title drops to the muted text colour. By default the requirement is treated as locked: the button is blocked and the "More" dropdown is hidden, so `onEnable` and `moreControls` have nothing to act on. `onConfigure` is unreachable whenever `requirements` is set at all, locked or not, because the button only reads "Configure" when there is no outstanding requirement.
|
|
44
|
+
|
|
45
|
+
A blocked button keeps its place in the tab order and is described by the high-intent badge, so a keyboard or screen-reader user reaches it and hears why it will not act. Don't wrap it in your own `disabled` handling, which would undo that.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { __ } from '@wordpress/i18n';
|
|
49
|
+
|
|
50
|
+
<CardFeature
|
|
51
|
+
title={ __( 'Metered countdown', 'newspack-plugin' ) }
|
|
52
|
+
description={ __( 'Show a countdown banner letting readers know how many free views they have left.', 'newspack-plugin' ) }
|
|
53
|
+
requirements={ __( 'Requires an API-based ESP', 'newspack-plugin' ) }
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Set `requirementsActionable` when the button is how the reader clears the requirement. It stays clickable and routes to `onEnable`, and an enabled card keeps its "More" dropdown so the feature can still be turned off.
|
|
44
58
|
|
|
45
59
|
```tsx
|
|
46
60
|
import { __ } from '@wordpress/i18n';
|
|
@@ -50,8 +64,9 @@ import { __ } from '@wordpress/i18n';
|
|
|
50
64
|
description={ __( 'Show a countdown banner letting readers know how many free views they have left.', 'newspack-plugin' ) }
|
|
51
65
|
enabled={ isEnabled }
|
|
52
66
|
requirements={ __( 'Requires metering', 'newspack-plugin' ) }
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
requirementsActionable
|
|
68
|
+
enableLabel={ __( 'Set up metering', 'newspack-plugin' ) }
|
|
69
|
+
onEnable={ () => history.push( '/settings/metering' ) }
|
|
55
70
|
moreControls={ [
|
|
56
71
|
{ title: __( 'Disable', 'newspack-plugin' ), onClick: () => setEnabled( false ) },
|
|
57
72
|
] }
|
|
@@ -60,35 +75,50 @@ import { __ } from '@wordpress/i18n';
|
|
|
60
75
|
|
|
61
76
|
## With a custom icon
|
|
62
77
|
|
|
63
|
-
|
|
78
|
+
`icon` takes either a descriptor object or a ready React element. A descriptor gets the standard treatment: pass `node` for the icon element, `fill` for the SVG colour, `backgroundColor` for a container background, and `radius` for the corner treatment. A ready element renders exactly as given, with no container, background or radius, which is the escape hatch for an icon that already carries its own chrome.
|
|
79
|
+
|
|
80
|
+
A descriptor's container is always **40 × 40 px** with the SVG at **24 × 24 px**. Setting `backgroundColor` without a `radius` gives 2px corners; pass `radius: 'full'` for a circle.
|
|
64
81
|
|
|
65
|
-
|
|
82
|
+
`fill` sets the container's `color`, which the SVG picks up through `fill: currentcolor`. That only recolours single-colour icons that inherit their fill, such as those from `@wordpress/icons`. A vendor's own mark carries `fill` on its paths and keeps its colours, so pair it with `backgroundColor` rather than trying to tint it.
|
|
83
|
+
|
|
84
|
+
`fill` and `backgroundColor` take any CSS colour. Reach for the Newspack palette when the icon should read as ours, and pass a literal when it should carry a third party's colour.
|
|
66
85
|
|
|
67
86
|
```tsx
|
|
68
87
|
import { __ } from '@wordpress/i18n';
|
|
69
88
|
import { Icon, starFilled } from '@wordpress/icons';
|
|
89
|
+
import colors from 'newspack-colors';
|
|
70
90
|
|
|
71
|
-
//
|
|
91
|
+
// Newspack palette, fill only
|
|
72
92
|
<CardFeature
|
|
73
93
|
title={ __( 'Content gifting', 'newspack-plugin' ) }
|
|
74
94
|
description={ __( 'Let subscribers share gated articles with non-subscribers.', 'newspack-plugin' ) }
|
|
75
|
-
icon={ { node: <Icon icon={ starFilled } />, fill: '
|
|
95
|
+
icon={ { node: <Icon icon={ starFilled } />, fill: colors[ 'primary-600' ] } }
|
|
76
96
|
enabled={ isEnabled }
|
|
77
97
|
onEnable={ handleEnable }
|
|
78
98
|
onConfigure={ handleConfigure }
|
|
79
99
|
moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
|
|
80
100
|
/>
|
|
81
101
|
|
|
82
|
-
//
|
|
102
|
+
// A vendor mark on its own brand background, keeping the mark's colours
|
|
83
103
|
<CardFeature
|
|
84
|
-
title={ __( '
|
|
85
|
-
description={ __( '
|
|
86
|
-
icon={ { node: <
|
|
104
|
+
title={ __( 'Mailchimp', 'newspack-plugin' ) }
|
|
105
|
+
description={ __( 'Sync reader activity with your Mailchimp audience.', 'newspack-plugin' ) }
|
|
106
|
+
icon={ { node: <MailchimpMark />, backgroundColor: '#ffe01b', radius: 'full' } }
|
|
87
107
|
enabled={ isEnabled }
|
|
88
108
|
onEnable={ handleEnable }
|
|
89
109
|
onConfigure={ handleConfigure }
|
|
90
110
|
moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
|
|
91
111
|
/>
|
|
112
|
+
|
|
113
|
+
// A ready element, rendered as-is
|
|
114
|
+
<CardFeature
|
|
115
|
+
title={ __( 'Mailchimp', 'newspack-plugin' ) }
|
|
116
|
+
description={ __( 'Sync reader activity with your Mailchimp audience.', 'newspack-plugin' ) }
|
|
117
|
+
icon={ <IntegrationIcon provider="mailchimp" /> }
|
|
118
|
+
enabled={ isEnabled }
|
|
119
|
+
onEnable={ handleEnable }
|
|
120
|
+
onConfigure={ handleConfigure }
|
|
121
|
+
/>
|
|
92
122
|
```
|
|
93
123
|
|
|
94
124
|
## With custom button labels
|
|
@@ -112,7 +142,7 @@ import { __ } from '@wordpress/i18n';
|
|
|
112
142
|
|
|
113
143
|
## With a custom badge
|
|
114
144
|
|
|
115
|
-
Override `
|
|
145
|
+
Override `badge` to change the badge shown when the feature is enabled. See [`BadgeIntent`](../types.ts) for the available intents.
|
|
116
146
|
|
|
117
147
|
```tsx
|
|
118
148
|
import { __ } from '@wordpress/i18n';
|
|
@@ -121,8 +151,7 @@ import { __ } from '@wordpress/i18n';
|
|
|
121
151
|
title={ __( 'Stripe', 'newspack-plugin' ) }
|
|
122
152
|
description={ __( 'Accept payments via Stripe.', 'newspack-plugin' ) }
|
|
123
153
|
enabled={ isEnabled }
|
|
124
|
-
|
|
125
|
-
badgeLevel="info"
|
|
154
|
+
badge={ { label: __( 'Live mode', 'newspack-plugin' ), intent: 'informational' } }
|
|
126
155
|
onEnable={ handleEnable }
|
|
127
156
|
onConfigure={ () => history.push( '/settings/stripe' ) }
|
|
128
157
|
moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
|
|
@@ -150,23 +179,34 @@ import { __ } from '@wordpress/i18n';
|
|
|
150
179
|
/>
|
|
151
180
|
```
|
|
152
181
|
|
|
182
|
+
## What the layout rests on
|
|
183
|
+
|
|
184
|
+
The card is built on `Card.Root`, `Card.Header` and `Card.Content` from `@wordpress/ui`. Three things about that pairing decide how it renders.
|
|
185
|
+
|
|
186
|
+
**Cards in a row bottom-align their action rows.** `Card.Root` is `display: flex; flex-direction: column`, and `.newspack-card-feature__actions` takes `margin-top: auto`. Two cards with descriptions of different lengths still put their buttons on one baseline. No test covers this, because jsdom computes no layout. If a library update drops the column direction, the cards stop aligning and nothing fails.
|
|
187
|
+
|
|
188
|
+
**The action row sits in `Card.Content`, which the library documents as the main content area.** The description sits in `Card.Header`, so the two are inverted. The gap between them lands at zero while `--wpds-dimension-gap-xl` and `--wpds-dimension-padding-2xl` are both 24px, and `margin-top: auto` is unlayered, so it wins over the library's layered rule either way. Move the action row when the library ships a `Card.Footer`.
|
|
189
|
+
|
|
190
|
+
**The title keeps its own type size.** `Card.Title` renders at `heading-lg`, which is 15px, and this title is 20px. It uses `heading-x-large()` from `@wordpress/base-styles` instead. Switch to `<Card.Title render={ createElement( 'h3' ) }>` if the two sizes ever agree.
|
|
191
|
+
|
|
153
192
|
## Props
|
|
154
193
|
|
|
155
194
|
| Prop | Type | Default | Description |
|
|
156
195
|
|---|---|---|---|
|
|
157
196
|
| `title` | `string` | — | Card heading (**required**) |
|
|
197
|
+
| `headingLevel` | `2`–`6` | `3` | Heading level for the title. The default sits under a `SectionHeader` or a `WizardsTab` heading, both of which are h2. Raise it when the cards sit under a deeper section label |
|
|
158
198
|
| `description` | `string` | — | Supporting text below the title |
|
|
159
|
-
| `icon` | `CardFeatureIcon` | — | Icon displayed on the right. See `CardFeatureIcon` below. |
|
|
199
|
+
| `icon` | `CardFeatureIcon \| ReactElement` | — | Icon displayed on the right. A descriptor gets the 40 × 40 container; a ready element renders as-is. See `CardFeatureIcon` below. |
|
|
160
200
|
| `enabled` | `boolean` | `false` | Whether the feature is currently enabled |
|
|
161
|
-
| `requirements` | `string` | — | When set, enters the unmet-requirements state
|
|
201
|
+
| `requirements` | `string` | — | When set, enters the unmet-requirements state. The value is the high-intent badge text and the primary button's accessible description, so write it to read sensibly after the button's label |
|
|
162
202
|
| `requirementsActionable` | `boolean` | `false` | When `requirements` is set, keep the primary button clickable so it can remediate the unmet requirement, and keep the "More" dropdown visible on an enabled card (degraded but still operable) |
|
|
163
|
-
| `enableLabel` | `string` | `"Enable"` |
|
|
164
|
-
| `configureLabel` | `string` | `"Configure"` |
|
|
165
|
-
| `onEnable` | `() => void` | — | Called when the primary button is clicked and the feature is
|
|
166
|
-
| `onConfigure` | `() => void` | — | Called when the primary button is clicked
|
|
203
|
+
| `enableLabel` | `string` | `"Enable"` | Label for the primary button in its "Enable" states: not enabled, or enabled with an unmet requirement |
|
|
204
|
+
| `configureLabel` | `string` | `"Configure"` | Label for the primary button in its "Configure" state: enabled, with no unmet requirement |
|
|
205
|
+
| `onEnable` | `() => void` | — | Called when the primary button is clicked while it reads "Enable". That covers the not-enabled case and the enabled-with-unmet-requirements case, where the feature is on but the requirement is what the button acts on |
|
|
206
|
+
| `onConfigure` | `() => void` | — | Called when the primary button is clicked while it reads "Configure", which is the enabled state with no unmet requirements |
|
|
167
207
|
| `moreControls` | `MoreControl[]` | — | Items for the "More" dropdown. Shown when `enabled` and either there are no `requirements` or `requirementsActionable` is set |
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
208
|
+
| `badge` | `{ label?: string; intent?: BadgeIntent }` | `{ label: "Enabled", intent: "stable" }` | Badge shown when enabled. Ignored while `requirements` is set, which takes the badge |
|
|
209
|
+
| `busy` | `boolean` | `false` | Shows the primary button as busy and blocks it while an action is in flight |
|
|
170
210
|
| `className` | `string` | — | Additional class name applied to the card element |
|
|
171
211
|
|
|
172
212
|
### `CardFeatureIcon`
|
|
@@ -177,7 +217,8 @@ type CardFeatureIcon = {
|
|
|
177
217
|
fill?: string; // SVG fill colour (applied via currentColor)
|
|
178
218
|
backgroundColor?: string; // Background colour of the 40×40 container
|
|
179
219
|
radius?: 'small' | 'full'; // 'small' = 2px ($radius-small), 'full' = 50% ($radius-round)
|
|
180
|
-
//
|
|
220
|
+
// Defaults to 'small' whenever backgroundColor is set,
|
|
221
|
+
// and has nothing to round without one.
|
|
181
222
|
};
|
|
182
223
|
```
|
|
183
224
|
|
|
@@ -187,6 +228,6 @@ type CardFeatureIcon = {
|
|
|
187
228
|
type MoreControl = {
|
|
188
229
|
title: string;
|
|
189
230
|
onClick: () => void;
|
|
190
|
-
icon?:
|
|
231
|
+
icon?: JSX.Element;
|
|
191
232
|
};
|
|
192
233
|
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies.
|
|
3
3
|
*/
|
|
4
|
-
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* WordPress dependencies.
|
|
@@ -20,46 +20,261 @@ jest.mock( '@wordpress/i18n', () => {
|
|
|
20
20
|
return { ...actual, _x: jest.fn( text => text ) };
|
|
21
21
|
} );
|
|
22
22
|
|
|
23
|
+
const primaryButton = () => screen.getByRole( 'button', { name: /Enable|Configure/ } );
|
|
24
|
+
const moreMenu = () => screen.queryByRole( 'button', { name: /^More options for/ } );
|
|
25
|
+
|
|
23
26
|
describe( 'CardFeature', () => {
|
|
24
27
|
beforeEach( () => {
|
|
25
28
|
_x.mockClear();
|
|
26
29
|
} );
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
describe( 'structure', () => {
|
|
32
|
+
it( 'renders the title as a level-3 heading and the description alongside it', () => {
|
|
33
|
+
render( <CardFeature title="Content gifting" description="Let subscribers share gated articles." /> );
|
|
34
|
+
expect( screen.getByRole( 'heading', { level: 3 } ) ).toHaveTextContent( 'Content gifting' );
|
|
35
|
+
expect( screen.getByText( 'Let subscribers share gated articles.' ) ).toBeInTheDocument();
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'renders the title at the requested heading level', () => {
|
|
39
|
+
render( <CardFeature title="Content gifting" headingLevel={ 4 } /> );
|
|
40
|
+
expect( screen.getByRole( 'heading', { level: 4 } ) ).toHaveTextContent( 'Content gifting' );
|
|
41
|
+
expect( screen.queryByRole( 'heading', { level: 3 } ) ).not.toBeInTheDocument();
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
it( 'omits the description paragraph when none is passed', () => {
|
|
45
|
+
const { container } = render( <CardFeature title="Content gifting" /> );
|
|
46
|
+
expect( container.querySelector( '.newspack-card-feature__description' ) ).toBeNull();
|
|
47
|
+
} );
|
|
33
48
|
} );
|
|
34
49
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
describe( 'accessible names', () => {
|
|
51
|
+
it( 'names the feature in the primary button, keeping the visible label first', () => {
|
|
52
|
+
render( <CardFeature title="Metered Countdown" /> );
|
|
53
|
+
const button = screen.getByRole( 'button', { name: 'Enable Metered Countdown' } );
|
|
54
|
+
expect( button ).toHaveTextContent( 'Enable' );
|
|
55
|
+
expect( button.getAttribute( 'aria-label' ).startsWith( button.textContent ) ).toBe( true );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'carries a custom label into the accessible name', () => {
|
|
59
|
+
render( <CardFeature title="Subscription retention" enableLabel="Change" /> );
|
|
60
|
+
expect( screen.getByRole( 'button', { name: 'Change Subscription retention' } ) ).toHaveTextContent( 'Change' );
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
it( 'names the feature in the configure state too', () => {
|
|
64
|
+
render( <CardFeature title="Content Gifting" enabled /> );
|
|
65
|
+
expect( screen.getByRole( 'button', { name: 'Configure Content Gifting' } ) ).toBeInTheDocument();
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
it( 'names the feature in the More menu', () => {
|
|
69
|
+
render( <CardFeature title="Content Gifting" enabled moreControls={ [ { title: 'Disable', onClick: () => {} } ] } /> );
|
|
70
|
+
expect( screen.getByRole( 'button', { name: 'More options for Content Gifting' } ) ).toBeInTheDocument();
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
it( 'gives the accessible-name order its own catalogue entry', () => {
|
|
74
|
+
render( <CardFeature title="Metered Countdown" /> );
|
|
75
|
+
expect( _x ).toHaveBeenCalledWith( '%1$s %2$s', 'accessible button name: visible action label, then feature name', 'newspack-plugin' );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'distinguishes two cards that share a button label', () => {
|
|
79
|
+
render(
|
|
80
|
+
<>
|
|
81
|
+
<CardFeature title="Metered Countdown" />
|
|
82
|
+
<CardFeature title="Content Gifting" />
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
expect( screen.getByRole( 'button', { name: 'Enable Metered Countdown' } ) ).toBeInTheDocument();
|
|
86
|
+
expect( screen.getByRole( 'button', { name: 'Enable Content Gifting' } ) ).toBeInTheDocument();
|
|
87
|
+
} );
|
|
38
88
|
} );
|
|
39
89
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
90
|
+
describe( 'primary button', () => {
|
|
91
|
+
it( 'reads Enable and calls onEnable when the feature is off', () => {
|
|
92
|
+
const onEnable = jest.fn();
|
|
93
|
+
const onConfigure = jest.fn();
|
|
94
|
+
render( <CardFeature title="Content gifting" onEnable={ onEnable } onConfigure={ onConfigure } /> );
|
|
95
|
+
expect( primaryButton() ).toHaveTextContent( 'Enable' );
|
|
96
|
+
fireEvent.click( primaryButton() );
|
|
97
|
+
expect( onEnable ).toHaveBeenCalledTimes( 1 );
|
|
98
|
+
expect( onConfigure ).not.toHaveBeenCalled();
|
|
99
|
+
} );
|
|
100
|
+
|
|
101
|
+
it( 'reads Configure and calls onConfigure once enabled', () => {
|
|
102
|
+
const onEnable = jest.fn();
|
|
103
|
+
const onConfigure = jest.fn();
|
|
104
|
+
render( <CardFeature title="Content gifting" enabled onEnable={ onEnable } onConfigure={ onConfigure } /> );
|
|
105
|
+
expect( primaryButton() ).toHaveTextContent( 'Configure' );
|
|
106
|
+
fireEvent.click( primaryButton() );
|
|
107
|
+
expect( onConfigure ).toHaveBeenCalledTimes( 1 );
|
|
108
|
+
expect( onEnable ).not.toHaveBeenCalled();
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
it( 'still routes to onEnable when enabled with an unmet requirement, since the button reads Enable', () => {
|
|
112
|
+
const onEnable = jest.fn();
|
|
113
|
+
const onConfigure = jest.fn();
|
|
114
|
+
render(
|
|
115
|
+
<CardFeature
|
|
116
|
+
title="Content gifting"
|
|
117
|
+
enabled
|
|
118
|
+
requirements="Requires metering"
|
|
119
|
+
requirementsActionable
|
|
120
|
+
onEnable={ onEnable }
|
|
121
|
+
onConfigure={ onConfigure }
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
expect( primaryButton() ).toHaveTextContent( 'Enable' );
|
|
125
|
+
fireEvent.click( primaryButton() );
|
|
126
|
+
expect( onEnable ).toHaveBeenCalledTimes( 1 );
|
|
127
|
+
expect( onConfigure ).not.toHaveBeenCalled();
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'keeps the button live on an actionable requirement before the feature is on, which is how Activate reaches the user', () => {
|
|
131
|
+
const onEnable = jest.fn();
|
|
132
|
+
render(
|
|
133
|
+
<CardFeature
|
|
134
|
+
title="Mailchimp"
|
|
135
|
+
requirements="Requires WooCommerce"
|
|
136
|
+
requirementsActionable
|
|
137
|
+
enableLabel="Activate"
|
|
138
|
+
onEnable={ onEnable }
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
const button = screen.getByRole( 'button', { name: 'Activate Mailchimp' } );
|
|
142
|
+
expect( button ).not.toHaveAttribute( 'aria-disabled' );
|
|
143
|
+
fireEvent.click( button );
|
|
144
|
+
expect( onEnable ).toHaveBeenCalledTimes( 1 );
|
|
145
|
+
expect( screen.getByText( 'Requires WooCommerce' ) ).toBeInTheDocument();
|
|
146
|
+
} );
|
|
147
|
+
|
|
148
|
+
it( 'does not fire when a requirement is not actionable, but stays reachable', () => {
|
|
149
|
+
const onEnable = jest.fn();
|
|
150
|
+
render( <CardFeature title="Content gifting" requirements="Managed by site configuration" onEnable={ onEnable } /> );
|
|
151
|
+
expect( primaryButton() ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
152
|
+
expect( primaryButton() ).not.toHaveAttribute( 'disabled' );
|
|
153
|
+
fireEvent.click( primaryButton() );
|
|
154
|
+
expect( onEnable ).not.toHaveBeenCalled();
|
|
155
|
+
} );
|
|
156
|
+
|
|
157
|
+
it( 'points the blocked button at the badge that explains why', () => {
|
|
158
|
+
render( <CardFeature title="Content gifting" requirements="Managed by site configuration" /> );
|
|
159
|
+
const describedBy = primaryButton().getAttribute( 'aria-describedby' );
|
|
160
|
+
expect( describedBy ).toBeTruthy();
|
|
161
|
+
expect( document.getElementById( describedBy ) ).toHaveTextContent( 'Managed by site configuration' );
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
it( 'leaves the enabled badge unlinked, since it explains nothing about the button', () => {
|
|
165
|
+
render( <CardFeature title="Content gifting" enabled /> );
|
|
166
|
+
expect( primaryButton() ).not.toHaveAttribute( 'aria-describedby' );
|
|
167
|
+
} );
|
|
168
|
+
|
|
169
|
+
it( 'disables the button while an action is in flight, even with an actionable requirement', () => {
|
|
170
|
+
const onEnable = jest.fn();
|
|
171
|
+
const { rerender } = render( <CardFeature title="Content gifting" busy onEnable={ onEnable } /> );
|
|
172
|
+
expect( primaryButton() ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
173
|
+
fireEvent.click( primaryButton() );
|
|
174
|
+
expect( onEnable ).not.toHaveBeenCalled();
|
|
175
|
+
|
|
176
|
+
rerender( <CardFeature title="Content gifting" busy requirements="Requires metering" requirementsActionable onEnable={ onEnable } /> );
|
|
177
|
+
expect( primaryButton() ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
178
|
+
} );
|
|
179
|
+
|
|
180
|
+
it( 'accepts custom labels for both states', () => {
|
|
181
|
+
const { rerender } = render( <CardFeature title="Apple News" enableLabel="Connect" configureLabel="Manage connection" /> );
|
|
182
|
+
expect( screen.getByRole( 'button', { name: 'Connect Apple News' } ) ).toBeInTheDocument();
|
|
183
|
+
rerender( <CardFeature title="Apple News" enabled enableLabel="Connect" configureLabel="Manage connection" /> );
|
|
184
|
+
expect( screen.getByRole( 'button', { name: 'Manage connection Apple News' } ) ).toBeInTheDocument();
|
|
185
|
+
} );
|
|
186
|
+
} );
|
|
187
|
+
|
|
188
|
+
describe( 'badge', () => {
|
|
189
|
+
it( 'shows nothing when off, and the enabled badge when on', () => {
|
|
190
|
+
const { rerender } = render( <CardFeature title="Content gifting" /> );
|
|
191
|
+
expect( screen.queryByText( 'Enabled' ) ).not.toBeInTheDocument();
|
|
192
|
+
rerender( <CardFeature title="Content gifting" enabled /> );
|
|
193
|
+
expect( screen.getByText( 'Enabled' ) ).toBeInTheDocument();
|
|
194
|
+
} );
|
|
195
|
+
|
|
196
|
+
it( 'lets the requirements badge win over the enabled badge', () => {
|
|
197
|
+
render( <CardFeature title="Content gifting" enabled requirements="Requires metering" /> );
|
|
198
|
+
expect( screen.getByText( 'Requires metering' ) ).toBeInTheDocument();
|
|
199
|
+
expect( screen.queryByText( 'Enabled' ) ).not.toBeInTheDocument();
|
|
200
|
+
} );
|
|
201
|
+
|
|
202
|
+
it( 'accepts custom badge text', () => {
|
|
203
|
+
render( <CardFeature title="Stripe" enabled badge={ { label: 'Live mode', intent: 'informational' } } /> );
|
|
204
|
+
expect( screen.getByText( 'Live mode' ) ).toBeInTheDocument();
|
|
205
|
+
} );
|
|
206
|
+
} );
|
|
207
|
+
|
|
208
|
+
describe( 'More menu', () => {
|
|
209
|
+
const controls = [ { title: 'Disable', onClick: jest.fn() } ];
|
|
210
|
+
|
|
211
|
+
it( 'appears only when enabled and controls are supplied', () => {
|
|
212
|
+
const { rerender } = render( <CardFeature title="Content gifting" moreControls={ controls } /> );
|
|
213
|
+
expect( moreMenu() ).not.toBeInTheDocument();
|
|
214
|
+
rerender( <CardFeature title="Content gifting" enabled /> );
|
|
215
|
+
expect( moreMenu() ).not.toBeInTheDocument();
|
|
216
|
+
rerender( <CardFeature title="Content gifting" enabled moreControls={ controls } /> );
|
|
217
|
+
expect( moreMenu() ).toBeInTheDocument();
|
|
218
|
+
} );
|
|
219
|
+
|
|
220
|
+
it( 'stays available on an actionable requirement and hides on a locked one', () => {
|
|
221
|
+
const { rerender } = render(
|
|
222
|
+
<CardFeature title="Content gifting" enabled requirements="Requires metering" requirementsActionable moreControls={ controls } />
|
|
223
|
+
);
|
|
224
|
+
expect( moreMenu() ).toBeInTheDocument();
|
|
225
|
+
rerender( <CardFeature title="Content gifting" enabled requirements="Managed by site configuration" moreControls={ controls } /> );
|
|
226
|
+
expect( moreMenu() ).not.toBeInTheDocument();
|
|
227
|
+
} );
|
|
43
228
|
} );
|
|
44
229
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
230
|
+
describe( 'icon', () => {
|
|
231
|
+
it( 'renders a ready element as-is, without the descriptor container', () => {
|
|
232
|
+
const { container } = render( <CardFeature title="Content gifting" icon={ <span data-testid="ready-icon" /> } /> );
|
|
233
|
+
expect( screen.getByTestId( 'ready-icon' ) ).toBeInTheDocument();
|
|
234
|
+
expect( container.querySelector( '.newspack-card-feature__icon' ) ).toBeNull();
|
|
235
|
+
} );
|
|
236
|
+
|
|
237
|
+
it( 'applies the descriptor colours inline and rounds fully on request', () => {
|
|
238
|
+
const { container } = render(
|
|
239
|
+
<CardFeature
|
|
240
|
+
title="Content gifting"
|
|
241
|
+
icon={ { node: <span data-testid="descriptor-icon" />, fill: '#003da5', backgroundColor: '#dfe7f4', radius: 'full' } }
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
const iconContainer = container.querySelector( '.newspack-card-feature__icon' );
|
|
245
|
+
expect( screen.getByTestId( 'descriptor-icon' ) ).toBeInTheDocument();
|
|
246
|
+
expect( iconContainer ).toHaveClass( 'newspack-card-feature__icon--radius-full' );
|
|
247
|
+
expect( iconContainer ).toHaveStyle( { backgroundColor: '#dfe7f4', color: '#003da5' } );
|
|
248
|
+
} );
|
|
249
|
+
|
|
250
|
+
it( 'hides the descriptor container from assistive tech, since the title already names the feature', () => {
|
|
251
|
+
const { container } = render( <CardFeature title="Content gifting" icon={ { node: <svg /> } } /> );
|
|
252
|
+
expect( container.querySelector( '.newspack-card-feature__icon' ) ).toHaveAttribute( 'aria-hidden', 'true' );
|
|
253
|
+
} );
|
|
254
|
+
|
|
255
|
+
it( 'falls back to small corners when a background is set without a radius', () => {
|
|
256
|
+
const { container } = render( <CardFeature title="Content gifting" icon={ { node: <span />, backgroundColor: '#dfe7f4' } } /> );
|
|
257
|
+
const iconContainer = container.querySelector( '.newspack-card-feature__icon' );
|
|
258
|
+
expect( iconContainer ).toHaveClass( 'newspack-card-feature__icon--radius-small' );
|
|
259
|
+
expect( iconContainer ).not.toHaveClass( 'newspack-card-feature__icon--radius-full' );
|
|
260
|
+
} );
|
|
261
|
+
|
|
262
|
+
it( 'leaves an unbacked descriptor icon without a radius class', () => {
|
|
263
|
+
const { container } = render( <CardFeature title="Content gifting" icon={ { node: <span />, fill: '#003da5' } } /> );
|
|
264
|
+
const iconContainer = container.querySelector( '.newspack-card-feature__icon' );
|
|
265
|
+
expect( iconContainer ).not.toHaveClass( 'newspack-card-feature__icon--radius-small' );
|
|
266
|
+
} );
|
|
48
267
|
} );
|
|
49
268
|
|
|
50
|
-
it( '
|
|
51
|
-
render( <CardFeature title="
|
|
52
|
-
expect(
|
|
269
|
+
it( 'passes className through to the card element', () => {
|
|
270
|
+
const { container } = render( <CardFeature title="Content gifting" className="newspack-subscribers__card" /> );
|
|
271
|
+
expect( container.querySelector( '.newspack-card-feature' ) ).toHaveClass( 'newspack-subscribers__card' );
|
|
53
272
|
} );
|
|
54
273
|
|
|
55
|
-
it( '
|
|
56
|
-
render(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
</>
|
|
61
|
-
);
|
|
62
|
-
expect( screen.getByRole( 'button', { name: 'Enable Metered Countdown' } ) ).toBeInTheDocument();
|
|
63
|
-
expect( screen.getByRole( 'button', { name: 'Enable Content Gifting' } ) ).toBeInTheDocument();
|
|
274
|
+
it( 'marks the card as muted only when requirements are set', () => {
|
|
275
|
+
const { container, rerender } = render( <CardFeature title="Content gifting" enabled /> );
|
|
276
|
+
expect( container.querySelector( '.newspack-card-feature--muted' ) ).toBeNull();
|
|
277
|
+
rerender( <CardFeature title="Content gifting" enabled requirements="Requires metering" /> );
|
|
278
|
+
expect( container.querySelector( '.newspack-card-feature--muted' ) ).toBeInTheDocument();
|
|
64
279
|
} );
|
|
65
280
|
} );
|