newspack-components 4.2.0 → 4.4.0-epic-editor-refactor.1
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 +11 -0
- package/DEVELOPMENT.md +868 -0
- package/copy-styles.js +3 -2
- package/dist/cjs/accordion/index.js +17 -12
- package/dist/cjs/action-card/index.js +186 -140
- package/dist/cjs/action-card/style.scss +25 -2
- package/dist/cjs/autocomplete-tokenfield/index.js +254 -234
- package/dist/cjs/autocomplete-with-latest-posts/index.js +351 -0
- package/dist/cjs/autocomplete-with-latest-posts/style.scss +150 -0
- package/dist/cjs/autocomplete-with-suggestions/index.js +229 -126
- package/dist/cjs/badge/index.js +32 -0
- package/dist/cjs/badge/style.scss +36 -0
- package/dist/cjs/box-contrast/index.js +19 -15
- package/dist/cjs/button/index.js +41 -24
- package/dist/cjs/button-props/button-props.js +5 -5
- package/dist/cjs/button-props/index.js +2 -2
- package/dist/cjs/card/core-card.js +199 -0
- package/dist/cjs/card/index.js +75 -35
- package/dist/cjs/card/style-core.scss +295 -0
- package/dist/cjs/card/style.scss +4 -4
- package/dist/cjs/card-feature/index.js +141 -0
- package/dist/cjs/card-feature/style.scss +75 -0
- package/dist/cjs/card-form/index.js +144 -0
- package/dist/cjs/card-form/style.scss +20 -0
- package/dist/cjs/card-settings-group/index.js +69 -0
- package/dist/cjs/card-settings-group/style.scss +33 -0
- package/dist/cjs/card-sortable-list/index.js +426 -0
- package/dist/cjs/card-sortable-list/style.scss +48 -0
- package/dist/cjs/category-autocomplete/index.js +175 -155
- package/dist/cjs/color-picker/index.js +41 -23
- package/dist/cjs/color-picker/index.test.js +82 -0
- package/dist/cjs/color-picker/style.scss +11 -13
- package/dist/cjs/confirm-dialog/index.js +123 -0
- package/dist/cjs/consts.js +1 -1
- package/dist/cjs/custom-select-control/index.js +39 -28
- package/dist/cjs/dataviews/index.js +38 -0
- package/dist/cjs/dataviews/style.scss +9 -0
- package/dist/cjs/divider/index.js +47 -0
- package/dist/cjs/divider/style.scss +51 -0
- package/dist/cjs/footer/index.js +36 -30
- package/dist/cjs/form-token-field/index.js +51 -40
- package/dist/cjs/form-token-field/style.scss +0 -5
- package/dist/cjs/global-notices/index.js +5 -5
- package/dist/cjs/grid/index.js +25 -18
- package/dist/cjs/grid/style.scss +67 -8
- package/dist/cjs/handoff/index.js +204 -156
- package/dist/cjs/handoff-message/index.js +12 -7
- package/dist/cjs/hooks/index.js +10 -8
- package/dist/cjs/hooks/use-confirm-dialog.js +79 -0
- package/dist/cjs/hooks/use-unsaved-changes-dialog.js +144 -0
- package/dist/cjs/hooks/useObjectState.js +21 -12
- package/dist/cjs/hooks/useObjectState.test.js +55 -33
- package/dist/cjs/hooks/useOnClickOutside.js +6 -6
- package/dist/cjs/hooks/usePrompt.js +10 -13
- package/dist/cjs/image-upload/index.js +114 -123
- package/dist/cjs/image-upload/index.test.js +11 -13
- package/dist/cjs/image-upload/style.scss +12 -40
- package/dist/cjs/index.js +166 -92
- package/dist/cjs/info-button/index.js +43 -32
- package/dist/cjs/integration-icons/active-campaign.js +22 -0
- package/dist/cjs/integration-icons/constant-contact.js +28 -0
- package/dist/cjs/integration-icons/fundraise-up.js +25 -0
- package/dist/cjs/integration-icons/index.js +48 -0
- package/dist/cjs/integration-icons/mailchimp.js +19 -0
- package/dist/cjs/integration-icons/salesforce.js +17 -0
- package/dist/cjs/integration-icons/wisepops.js +20 -0
- package/dist/cjs/modal/index.js +29 -19
- package/dist/cjs/modal/style.scss +47 -13
- package/dist/cjs/newspack-icon/index.js +45 -38
- package/dist/cjs/newspack-icon/style.scss +5 -3
- package/dist/cjs/notice/index.js +64 -55
- package/dist/cjs/plugin-installer/index.js +259 -253
- package/dist/cjs/plugin-settings/SettingsSection.js +66 -60
- package/dist/cjs/plugin-settings/index.js +217 -200
- package/dist/cjs/plugin-toggle/index.js +192 -199
- package/dist/cjs/popover/index.js +41 -28
- package/dist/cjs/position-control/index.js +24 -23
- package/dist/cjs/progress-bar/index.js +71 -59
- package/dist/cjs/progress-bar/index.test.js +62 -68
- package/dist/cjs/proxied-imports/router.js +3 -3
- package/dist/cjs/radio-control/index.js +40 -27
- package/dist/cjs/section-header/index.js +143 -39
- package/dist/cjs/section-header/style.scss +129 -4
- package/dist/cjs/select-control/ButtonGroupControl.js +26 -21
- package/dist/cjs/select-control/GroupedSelectControl.js +41 -35
- package/dist/cjs/select-control/index.js +52 -40
- package/dist/cjs/settings/MinMaxSetting.js +30 -20
- package/dist/cjs/settings/SettingsCard.js +20 -17
- package/dist/cjs/settings/SettingsSection.js +22 -21
- package/dist/cjs/settings/index.js +5 -5
- package/dist/cjs/sortable-newsletter-list-control/index.js +78 -57
- package/dist/cjs/steps-list/index.js +44 -34
- package/dist/cjs/steps-list-item/index.js +47 -39
- package/dist/cjs/style-card/index.js +70 -63
- package/dist/cjs/style.scss +2 -0
- package/dist/cjs/tabbed-navigation/index.js +38 -36
- package/dist/cjs/text-control/index.js +27 -25
- package/dist/cjs/utils/color.js +4 -4
- package/dist/cjs/utils/editor-toolbar-back-button.js +61 -0
- package/dist/cjs/utils/index.js +31 -19
- package/dist/cjs/utils/style.scss +8 -0
- package/dist/cjs/waiting/index.js +48 -35
- package/dist/cjs/waiting/style.scss +4 -0
- package/dist/cjs/web-preview/index.js +219 -154
- package/dist/cjs/web-preview/style.scss +20 -53
- package/dist/cjs/with-wizard/index.js +362 -364
- package/dist/cjs/with-wizard/style.scss +28 -60
- package/dist/cjs/with-wizard-screen/index.js +54 -58
- package/dist/cjs/with-wizard-screen/style.scss +77 -10
- package/dist/cjs/wizard/components/WizardError.js +20 -18
- package/dist/cjs/wizard/components/WizardSnackbar.js +73 -0
- package/dist/cjs/wizard/components/style.scss +48 -0
- package/dist/cjs/wizard/index.js +207 -64
- package/dist/cjs/wizard/store/index.js +191 -97
- package/dist/cjs/wizard/store/utils.js +13 -8
- package/dist/esm/accordion/index.js +14 -9
- package/dist/esm/action-card/index.js +178 -124
- package/dist/esm/action-card/style.scss +25 -2
- package/dist/esm/autocomplete-tokenfield/index.js +248 -222
- package/dist/esm/autocomplete-with-latest-posts/index.js +347 -0
- package/dist/esm/autocomplete-with-latest-posts/style.scss +150 -0
- package/dist/esm/autocomplete-with-suggestions/index.js +226 -119
- package/dist/esm/badge/index.js +24 -0
- package/dist/esm/badge/style.scss +36 -0
- package/dist/esm/box-contrast/index.js +15 -11
- package/dist/esm/button/index.js +39 -19
- package/dist/esm/button-props/button-props.js +1 -1
- package/dist/esm/card/core-card.js +195 -0
- package/dist/esm/card/index.js +70 -22
- package/dist/esm/card/style-core.scss +295 -0
- package/dist/esm/card/style.scss +4 -4
- package/dist/esm/card-feature/index.js +131 -0
- package/dist/esm/card-feature/style.scss +75 -0
- package/dist/esm/card-form/index.js +134 -0
- package/dist/esm/card-form/style.scss +20 -0
- package/dist/esm/card-settings-group/index.js +61 -0
- package/dist/esm/card-settings-group/style.scss +33 -0
- package/dist/esm/card-sortable-list/index.js +425 -0
- package/dist/esm/card-sortable-list/style.scss +48 -0
- package/dist/esm/category-autocomplete/index.js +168 -140
- package/dist/esm/color-picker/index.js +39 -21
- package/dist/esm/color-picker/index.test.js +78 -0
- package/dist/esm/color-picker/style.scss +11 -13
- package/dist/esm/confirm-dialog/index.js +116 -0
- package/dist/esm/consts.js +1 -1
- package/dist/esm/custom-select-control/index.js +34 -15
- package/dist/esm/dataviews/index.js +34 -0
- package/dist/esm/dataviews/style.scss +9 -0
- package/dist/esm/divider/index.js +42 -0
- package/dist/esm/divider/style.scss +51 -0
- package/dist/esm/footer/index.js +34 -28
- package/dist/esm/form-token-field/index.js +46 -27
- package/dist/esm/form-token-field/style.scss +0 -5
- package/dist/esm/global-notices/index.js +3 -3
- package/dist/esm/grid/index.js +23 -13
- package/dist/esm/grid/style.scss +67 -8
- package/dist/esm/handoff/index.js +199 -143
- package/dist/esm/handoff-message/index.js +10 -6
- package/dist/esm/hooks/index.js +8 -6
- package/dist/esm/hooks/use-confirm-dialog.js +73 -0
- package/dist/esm/hooks/use-unsaved-changes-dialog.js +136 -0
- package/dist/esm/hooks/useObjectState.js +19 -9
- package/dist/esm/hooks/useObjectState.test.js +54 -32
- package/dist/esm/hooks/useOnClickOutside.js +4 -4
- package/dist/esm/hooks/usePrompt.js +9 -11
- package/dist/esm/image-upload/index.js +110 -111
- package/dist/esm/image-upload/index.test.js +11 -13
- package/dist/esm/image-upload/style.scss +12 -40
- package/dist/esm/index.js +13 -1
- package/dist/esm/info-button/index.js +38 -19
- package/dist/esm/integration-icons/active-campaign.js +16 -0
- package/dist/esm/integration-icons/constant-contact.js +22 -0
- package/dist/esm/integration-icons/fundraise-up.js +19 -0
- package/dist/esm/integration-icons/index.js +6 -0
- package/dist/esm/integration-icons/mailchimp.js +13 -0
- package/dist/esm/integration-icons/salesforce.js +11 -0
- package/dist/esm/integration-icons/wisepops.js +14 -0
- package/dist/esm/modal/index.js +27 -13
- package/dist/esm/modal/style.scss +47 -13
- package/dist/esm/newspack-icon/index.js +40 -25
- package/dist/esm/newspack-icon/style.scss +5 -3
- package/dist/esm/notice/index.js +59 -42
- package/dist/esm/plugin-installer/index.js +255 -243
- package/dist/esm/plugin-settings/SettingsSection.js +61 -50
- package/dist/esm/plugin-settings/index.js +212 -189
- package/dist/esm/plugin-toggle/index.js +187 -190
- package/dist/esm/popover/index.js +35 -16
- package/dist/esm/position-control/index.js +22 -18
- package/dist/esm/progress-bar/index.js +66 -49
- package/dist/esm/progress-bar/index.test.js +61 -67
- package/dist/esm/radio-control/index.js +34 -15
- package/dist/esm/section-header/index.js +140 -38
- package/dist/esm/section-header/style.scss +129 -4
- package/dist/esm/select-control/ButtonGroupControl.js +23 -18
- package/dist/esm/select-control/GroupedSelectControl.js +38 -30
- package/dist/esm/select-control/index.js +47 -27
- package/dist/esm/settings/MinMaxSetting.js +27 -16
- package/dist/esm/settings/SettingsCard.js +18 -13
- package/dist/esm/settings/SettingsSection.js +20 -19
- package/dist/esm/settings/index.js +3 -3
- package/dist/esm/sortable-newsletter-list-control/index.js +74 -53
- package/dist/esm/steps-list/index.js +39 -21
- package/dist/esm/steps-list-item/index.js +42 -26
- package/dist/esm/style-card/index.js +65 -50
- package/dist/esm/style.scss +2 -0
- package/dist/esm/tabbed-navigation/index.js +35 -33
- package/dist/esm/text-control/index.js +25 -19
- package/dist/esm/utils/color.js +4 -4
- package/dist/esm/utils/editor-toolbar-back-button.js +53 -0
- package/dist/esm/utils/index.js +29 -16
- package/dist/esm/utils/style.scss +8 -0
- package/dist/esm/waiting/index.js +43 -22
- package/dist/esm/waiting/style.scss +4 -0
- package/dist/esm/web-preview/index.js +218 -147
- package/dist/esm/web-preview/style.scss +20 -53
- package/dist/esm/with-wizard/index.js +360 -358
- package/dist/esm/with-wizard/style.scss +28 -60
- package/dist/esm/with-wizard-screen/index.js +48 -46
- package/dist/esm/with-wizard-screen/style.scss +77 -10
- package/dist/esm/wizard/components/WizardError.js +18 -16
- package/dist/esm/wizard/components/WizardSnackbar.js +70 -0
- package/dist/esm/wizard/components/style.scss +48 -0
- package/dist/esm/wizard/index.js +207 -58
- package/dist/esm/wizard/store/index.js +189 -90
- package/dist/esm/wizard/store/utils.js +13 -6
- package/package.json +30 -17
- package/release.config.js +1 -0
- package/shared/author-context.js +26 -0
- package/shared/hooks/use-coauthors.js +388 -0
- package/shared/hooks/use-coauthors.test.js +850 -0
- package/shared/hooks/use-custom-byline.js +47 -0
- package/shared/hooks/use-custom-byline.test.js +133 -0
- package/shared/scss/_mixins.scss +54 -0
- package/src/accordion/index.js +8 -7
- package/src/action-card/action-card.d.ts +7 -3
- package/src/action-card/index.js +39 -12
- package/src/action-card/style.scss +25 -2
- package/src/autocomplete-tokenfield/index.js +2 -2
- package/src/autocomplete-with-latest-posts/index.js +290 -0
- package/src/autocomplete-with-latest-posts/style.scss +150 -0
- package/src/badge/index.tsx +26 -0
- package/src/badge/style.scss +36 -0
- package/src/button/index.tsx +3 -2
- package/src/card/core-card.js +224 -0
- package/src/card/index.js +40 -1
- package/src/card/style-core.scss +295 -0
- package/src/card/style.scss +4 -4
- package/src/card-feature/README.md +188 -0
- package/src/card-feature/index.tsx +186 -0
- package/src/card-feature/style.scss +75 -0
- package/src/card-form/README.md +113 -0
- package/src/card-form/index.tsx +128 -0
- package/src/card-form/style.scss +20 -0
- package/src/card-settings-group/README.md +43 -0
- package/src/card-settings-group/index.tsx +78 -0
- package/src/card-settings-group/style.scss +33 -0
- package/src/card-sortable-list/README.md +51 -0
- package/src/card-sortable-list/index.tsx +435 -0
- package/src/card-sortable-list/style.scss +48 -0
- package/src/color-picker/index.js +15 -8
- package/src/color-picker/index.test.js +52 -0
- package/src/color-picker/style.scss +11 -13
- package/src/confirm-dialog/README.md +128 -0
- package/src/confirm-dialog/index.tsx +132 -0
- package/src/dataviews/index.tsx +34 -0
- package/src/dataviews/style.scss +9 -0
- package/src/divider/README.md +25 -0
- package/src/divider/index.js +31 -0
- package/src/divider/style.scss +51 -0
- package/src/form-token-field/style.scss +0 -5
- package/src/grid/index.tsx +2 -1
- package/src/grid/style.scss +67 -8
- package/src/handoff/README.md +93 -0
- package/src/handoff/index.js +46 -8
- package/src/hooks/index.ts +2 -1
- package/src/hooks/use-confirm-dialog.tsx +80 -0
- package/src/hooks/use-unsaved-changes-dialog.tsx +140 -0
- package/src/image-upload/index.js +10 -15
- package/src/image-upload/style.scss +12 -40
- package/src/index.js +13 -1
- package/src/integration-icons/active-campaign.js +14 -0
- package/src/integration-icons/constant-contact.js +22 -0
- package/src/integration-icons/fundraise-up.js +18 -0
- package/src/integration-icons/index.js +6 -0
- package/src/integration-icons/mailchimp.js +10 -0
- package/src/integration-icons/salesforce.js +10 -0
- package/src/integration-icons/wisepops.js +14 -0
- package/src/modal/index.js +12 -3
- package/src/modal/style.scss +47 -13
- package/src/newspack-icon/style.scss +5 -3
- package/src/plugin-installer/index.js +30 -19
- package/src/section-header/index.js +105 -14
- package/src/section-header/style.scss +129 -4
- package/src/select-control/index.js +4 -0
- package/src/style.scss +2 -0
- package/src/utils/editor-toolbar-back-button.tsx +48 -0
- package/src/utils/index.js +6 -0
- package/src/utils/style.scss +8 -0
- package/src/waiting/index.js +2 -1
- package/src/waiting/style.scss +4 -0
- package/src/web-preview/index.js +90 -35
- package/src/web-preview/style.scss +20 -53
- package/src/with-wizard/index.js +1 -1
- package/src/with-wizard/style.scss +28 -60
- package/src/with-wizard-screen/style.scss +77 -10
- package/src/wizard/components/WizardSnackbar.js +58 -0
- package/src/wizard/components/style.scss +48 -0
- package/src/wizard/index.js +152 -21
- package/src/wizard/store/index.js +28 -2
- package/dist/cjs/button-card/index.js +0 -72
- package/dist/cjs/button-card/style.scss +0 -180
- package/dist/esm/button-card/index.js +0 -64
- package/dist/esm/button-card/style.scss +0 -180
- package/src/button-card/index.js +0 -52
- package/src/button-card/style.scss +0 -180
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# CardFeature
|
|
2
|
+
|
|
3
|
+
A card component for presenting a named feature or setting with a predictable, state-driven interaction model. It handles the primary button, an optional "More" dropdown, and a badge automatically based on the `enabled` and `requirements` props.
|
|
4
|
+
|
|
5
|
+
## Layout rules
|
|
6
|
+
|
|
7
|
+
- **Maximum 2 cards per row.** Use `<Grid columns={ 2 }>` — never 3 or more. Cards are designed to be read, not scanned, and 3+ columns makes them too narrow for the content.
|
|
8
|
+
- The icon is always displayed on the **right-hand side**, aligned to the top of the content.
|
|
9
|
+
|
|
10
|
+
## States
|
|
11
|
+
|
|
12
|
+
| State | Condition | Button | Dropdown | Badge |
|
|
13
|
+
|---|---|---|---|---|
|
|
14
|
+
| **Unmet requirements** | `requirements` is set | "Enable" — disabled (clickable if `requirementsActionable`) | Hidden | Error badge with `requirements` text |
|
|
15
|
+
| **Disabled** | `!enabled`, no requirements | "Enable" | Hidden | None |
|
|
16
|
+
| **Enabled** | `enabled`, no requirements | "Configure" | Shown if `moreControls` provided | Success badge ("Enabled") |
|
|
17
|
+
|
|
18
|
+
The card content (title + description) is visually muted (gray text, lighter border) when `requirements` is set.
|
|
19
|
+
|
|
20
|
+
## Basic usage
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { __ } from '@wordpress/i18n';
|
|
24
|
+
|
|
25
|
+
<CardFeature
|
|
26
|
+
title={ __( 'Metered countdown', 'newspack-plugin' ) }
|
|
27
|
+
description={ __( 'Show a countdown banner letting readers know how many free views they have left.', 'newspack-plugin' ) }
|
|
28
|
+
enabled={ isEnabled }
|
|
29
|
+
onEnable={ () => setEnabled( true ) }
|
|
30
|
+
onConfigure={ () => history.push( '/settings/countdown' ) }
|
|
31
|
+
moreControls={ [
|
|
32
|
+
{ title: __( 'Disable', 'newspack-plugin' ), onClick: () => setEnabled( false ) },
|
|
33
|
+
] }
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## With unmet requirements
|
|
38
|
+
|
|
39
|
+
When `requirements` is set the button is disabled and an error badge displays the string. The title and description are visually muted.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { __ } from '@wordpress/i18n';
|
|
43
|
+
|
|
44
|
+
<CardFeature
|
|
45
|
+
title={ __( 'Metered countdown', 'newspack-plugin' ) }
|
|
46
|
+
description={ __( 'Show a countdown banner letting readers know how many free views they have left.', 'newspack-plugin' ) }
|
|
47
|
+
enabled={ isEnabled }
|
|
48
|
+
requirements={ __( 'Requires metering', 'newspack-plugin' ) }
|
|
49
|
+
onEnable={ () => setEnabled( true ) }
|
|
50
|
+
onConfigure={ () => history.push( '/settings/countdown' ) }
|
|
51
|
+
moreControls={ [
|
|
52
|
+
{ title: __( 'Disable', 'newspack-plugin' ), onClick: () => setEnabled( false ) },
|
|
53
|
+
] }
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## With a custom icon
|
|
58
|
+
|
|
59
|
+
The `icon` prop accepts an object, not a bare node. Pass `node` for the icon element, `fill` to control the SVG colour (applied via `currentColor`), `backgroundColor` for a container background, and `radius` for the corner treatment.
|
|
60
|
+
|
|
61
|
+
The icon container is always **40 × 40 px** with the SVG at **24 × 24 px**. `radius` only applies when `backgroundColor` is set.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { __ } from '@wordpress/i18n';
|
|
65
|
+
import { Icon, starFilled } from '@wordpress/icons';
|
|
66
|
+
|
|
67
|
+
// Icon with fill only
|
|
68
|
+
<CardFeature
|
|
69
|
+
title={ __( 'Content gifting', 'newspack-plugin' ) }
|
|
70
|
+
description={ __( 'Let subscribers share gated articles with non-subscribers.', 'newspack-plugin' ) }
|
|
71
|
+
icon={ { node: <Icon icon={ starFilled } />, fill: '#003da5' } }
|
|
72
|
+
enabled={ isEnabled }
|
|
73
|
+
onEnable={ handleEnable }
|
|
74
|
+
onConfigure={ handleConfigure }
|
|
75
|
+
moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
// Icon with circular background
|
|
79
|
+
<CardFeature
|
|
80
|
+
title={ __( 'Content gifting', 'newspack-plugin' ) }
|
|
81
|
+
description={ __( 'Let subscribers share gated articles with non-subscribers.', 'newspack-plugin' ) }
|
|
82
|
+
icon={ { node: <Icon icon={ starFilled } />, fill: '#003da5', backgroundColor: '#dfe7f4', radius: 'full' } }
|
|
83
|
+
enabled={ isEnabled }
|
|
84
|
+
onEnable={ handleEnable }
|
|
85
|
+
onConfigure={ handleConfigure }
|
|
86
|
+
moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## With custom button labels
|
|
91
|
+
|
|
92
|
+
Override `enableLabel` and `configureLabel` to match the context of the feature.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { __ } from '@wordpress/i18n';
|
|
96
|
+
|
|
97
|
+
<CardFeature
|
|
98
|
+
title={ __( 'Apple News', 'newspack-plugin' ) }
|
|
99
|
+
description={ __( 'Automatically publish articles to Apple News.', 'newspack-plugin' ) }
|
|
100
|
+
enabled={ isEnabled }
|
|
101
|
+
enableLabel={ __( 'Connect', 'newspack-plugin' ) }
|
|
102
|
+
configureLabel={ __( 'Manage connection', 'newspack-plugin' ) }
|
|
103
|
+
onEnable={ handleConnect }
|
|
104
|
+
onConfigure={ () => history.push( '/settings/apple-news' ) }
|
|
105
|
+
moreControls={ [ { title: __( 'Disconnect', 'newspack-plugin' ), onClick: handleDisconnect } ] }
|
|
106
|
+
/>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## With a custom badge
|
|
110
|
+
|
|
111
|
+
Override `badgeText` and `badgeLevel` to change the badge shown when the feature is enabled. Available levels: `default`, `info`, `success`, `warning`, `error`.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { __ } from '@wordpress/i18n';
|
|
115
|
+
|
|
116
|
+
<CardFeature
|
|
117
|
+
title={ __( 'Stripe', 'newspack-plugin' ) }
|
|
118
|
+
description={ __( 'Accept payments via Stripe.', 'newspack-plugin' ) }
|
|
119
|
+
enabled={ isEnabled }
|
|
120
|
+
badgeText={ __( 'Live mode', 'newspack-plugin' ) }
|
|
121
|
+
badgeLevel="info"
|
|
122
|
+
onEnable={ handleEnable }
|
|
123
|
+
onConfigure={ () => history.push( '/settings/stripe' ) }
|
|
124
|
+
moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
|
|
125
|
+
/>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## With multiple "More" controls
|
|
129
|
+
|
|
130
|
+
`moreControls` accepts any number of items, each with a `title`, `onClick`, and an optional `icon`.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { __ } from '@wordpress/i18n';
|
|
134
|
+
|
|
135
|
+
<CardFeature
|
|
136
|
+
title={ __( 'Newsletters', 'newspack-plugin' ) }
|
|
137
|
+
description={ __( 'Send newsletters directly from the WordPress editor.', 'newspack-plugin' ) }
|
|
138
|
+
enabled={ isEnabled }
|
|
139
|
+
onEnable={ handleEnable }
|
|
140
|
+
onConfigure={ () => history.push( '/settings/newsletters' ) }
|
|
141
|
+
moreControls={ [
|
|
142
|
+
{ title: __( 'Edit', 'newspack-plugin' ), onClick: handleEdit },
|
|
143
|
+
{ title: __( 'Preview', 'newspack-plugin' ), onClick: handlePreview },
|
|
144
|
+
{ title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable },
|
|
145
|
+
] }
|
|
146
|
+
/>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Props
|
|
150
|
+
|
|
151
|
+
| Prop | Type | Default | Description |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| `title` | `string` | — | Card heading (**required**) |
|
|
154
|
+
| `description` | `string` | — | Supporting text below the title |
|
|
155
|
+
| `icon` | `CardFeatureIcon` | — | Icon displayed on the right. See `CardFeatureIcon` below. |
|
|
156
|
+
| `enabled` | `boolean` | `false` | Whether the feature is currently enabled |
|
|
157
|
+
| `requirements` | `string` | — | When set, enters the unmet-requirements state; value is used as the error badge text |
|
|
158
|
+
| `requirementsActionable` | `boolean` | `false` | When `requirements` is set, keep the primary button clickable so it can remediate the unmet requirement |
|
|
159
|
+
| `enableLabel` | `string` | `"Enable"` | Primary button label when not enabled |
|
|
160
|
+
| `configureLabel` | `string` | `"Configure"` | Primary button label when enabled |
|
|
161
|
+
| `onEnable` | `() => void` | — | Called when the primary button is clicked and the feature is not enabled |
|
|
162
|
+
| `onConfigure` | `() => void` | — | Called when the primary button is clicked and the feature is enabled |
|
|
163
|
+
| `moreControls` | `MoreControl[]` | — | Items for the "More" dropdown, shown only when enabled |
|
|
164
|
+
| `badgeText` | `string` | `"Enabled"` | Badge text shown when enabled |
|
|
165
|
+
| `badgeLevel` | `BadgeLevel` | `"success"` | Badge level shown when enabled |
|
|
166
|
+
| `className` | `string` | — | Additional class name applied to the card element |
|
|
167
|
+
|
|
168
|
+
### `CardFeatureIcon`
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
type CardFeatureIcon = {
|
|
172
|
+
node: React.ReactNode; // The icon element to render
|
|
173
|
+
fill?: string; // SVG fill colour (applied via currentColor)
|
|
174
|
+
backgroundColor?: string; // Background colour of the 40×40 container
|
|
175
|
+
radius?: 'small' | 'full'; // 'small' = 2px ($radius-small), 'full' = 50% ($radius-round)
|
|
176
|
+
// Only applied when backgroundColor is set.
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `MoreControl`
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
type MoreControl = {
|
|
184
|
+
title: string;
|
|
185
|
+
onClick: () => void;
|
|
186
|
+
icon?: React.ReactNode;
|
|
187
|
+
};
|
|
188
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import classnames from 'classnames';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WordPress dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { __ } from '@wordpress/i18n';
|
|
10
|
+
import { DropdownMenu, __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
11
|
+
import { moreVertical } from '@wordpress/icons';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Internal dependencies
|
|
15
|
+
*/
|
|
16
|
+
import Badge from '../badge';
|
|
17
|
+
import Button from '../button';
|
|
18
|
+
import Card from '../card';
|
|
19
|
+
import './style.scss';
|
|
20
|
+
|
|
21
|
+
type BadgeLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
|
22
|
+
|
|
23
|
+
type CardFeatureIcon = {
|
|
24
|
+
/** The icon node to render (e.g. a WordPress <Icon> component). */
|
|
25
|
+
node: React.ReactNode;
|
|
26
|
+
/** SVG fill colour, applied via currentColor. */
|
|
27
|
+
fill?: string;
|
|
28
|
+
/** Background colour for the icon container. */
|
|
29
|
+
backgroundColor?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Border-radius of the icon container.
|
|
32
|
+
* 'small' uses $radius-small (2px), 'full' uses $radius-round (50%).
|
|
33
|
+
* Only relevant when backgroundColor is set.
|
|
34
|
+
*/
|
|
35
|
+
radius?: 'small' | 'full';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type MoreControl = {
|
|
39
|
+
title: string;
|
|
40
|
+
onClick: () => void;
|
|
41
|
+
icon?: React.ReactNode;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type CardFeatureProps = {
|
|
45
|
+
title: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
/** Icon displayed on the right-hand side of the title and description. */
|
|
48
|
+
icon?: CardFeatureIcon;
|
|
49
|
+
/** Whether the feature is currently enabled. */
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* When set, the card enters the "unmet requirements" state: an error
|
|
53
|
+
* badge displays this string and the title/description are muted. By
|
|
54
|
+
* default the primary button is disabled — set `requirementsActionable`
|
|
55
|
+
* if the primary button is the remediation for the unmet requirement.
|
|
56
|
+
*/
|
|
57
|
+
requirements?: string;
|
|
58
|
+
/**
|
|
59
|
+
* When `requirements` is set, keep the primary button clickable so the
|
|
60
|
+
* user can remediate the unmet requirement from this card.
|
|
61
|
+
*/
|
|
62
|
+
requirementsActionable?: boolean;
|
|
63
|
+
/** Primary button label when not enabled. Default: "Enable". */
|
|
64
|
+
enableLabel?: string;
|
|
65
|
+
/** Primary button label when enabled. Default: "Configure". */
|
|
66
|
+
configureLabel?: string;
|
|
67
|
+
/** Called when the primary button is clicked and the feature is not enabled. */
|
|
68
|
+
onEnable?: () => void;
|
|
69
|
+
/** Called when the primary button is clicked and the feature is enabled. */
|
|
70
|
+
onConfigure?: () => void;
|
|
71
|
+
/** Controls rendered inside the "More" dropdown, shown only when enabled. */
|
|
72
|
+
moreControls?: MoreControl[];
|
|
73
|
+
/** Badge text shown when enabled. Default: "Enabled". */
|
|
74
|
+
badgeText?: string;
|
|
75
|
+
/** Badge level shown when enabled. Default: "success". */
|
|
76
|
+
badgeLevel?: BadgeLevel;
|
|
77
|
+
className?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* CardFeature component.
|
|
82
|
+
*
|
|
83
|
+
* A card for presenting a named feature or setting with a predictable
|
|
84
|
+
* action model: a primary button, an optional "More" dropdown when enabled,
|
|
85
|
+
* and an automatic badge reflecting the current state.
|
|
86
|
+
*/
|
|
87
|
+
const CardFeature = ( {
|
|
88
|
+
title,
|
|
89
|
+
description,
|
|
90
|
+
icon,
|
|
91
|
+
enabled = false,
|
|
92
|
+
requirements,
|
|
93
|
+
requirementsActionable = false,
|
|
94
|
+
enableLabel,
|
|
95
|
+
configureLabel,
|
|
96
|
+
onEnable,
|
|
97
|
+
onConfigure,
|
|
98
|
+
moreControls,
|
|
99
|
+
badgeText,
|
|
100
|
+
badgeLevel = 'success',
|
|
101
|
+
className,
|
|
102
|
+
}: CardFeatureProps ) => {
|
|
103
|
+
const isMuted = !! requirements;
|
|
104
|
+
const classes = classnames( 'newspack-card-feature', className, {
|
|
105
|
+
'newspack-card-feature--muted': isMuted,
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
let badge: { text: string; level: BadgeLevel } | undefined;
|
|
109
|
+
if ( requirements ) {
|
|
110
|
+
badge = { text: requirements, level: 'error' };
|
|
111
|
+
} else if ( enabled ) {
|
|
112
|
+
badge = { text: badgeText ?? __( 'Enabled', 'newspack-plugin' ), level: badgeLevel };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isConfigureState = enabled && ! requirements;
|
|
116
|
+
const buttonLabel = isConfigureState ? configureLabel ?? __( 'Configure', 'newspack-plugin' ) : enableLabel ?? __( 'Enable', 'newspack-plugin' );
|
|
117
|
+
|
|
118
|
+
const handleButtonClick = () => {
|
|
119
|
+
if ( isConfigureState ) {
|
|
120
|
+
onConfigure?.();
|
|
121
|
+
} else {
|
|
122
|
+
onEnable?.();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const iconClasses = icon
|
|
127
|
+
? classnames( 'newspack-card-feature__icon', {
|
|
128
|
+
'newspack-card-feature__icon--radius-small': !! icon.backgroundColor && icon.radius !== 'full',
|
|
129
|
+
'newspack-card-feature__icon--radius-full': icon.radius === 'full',
|
|
130
|
+
} )
|
|
131
|
+
: undefined;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Card
|
|
135
|
+
className={ classes }
|
|
136
|
+
__experimentalCoreCard
|
|
137
|
+
__experimentalCoreProps={ {
|
|
138
|
+
headerStyle: { padding: 32 },
|
|
139
|
+
header: (
|
|
140
|
+
<>
|
|
141
|
+
<HStack alignment="top" spacing={ 4 }>
|
|
142
|
+
<div className="newspack-card-feature__content">
|
|
143
|
+
<h2 className="newspack-card-feature__title">{ title }</h2>
|
|
144
|
+
{ description && <p className="newspack-card-feature__description">{ description }</p> }
|
|
145
|
+
</div>
|
|
146
|
+
{ icon && (
|
|
147
|
+
<div
|
|
148
|
+
className={ iconClasses }
|
|
149
|
+
style={ {
|
|
150
|
+
backgroundColor: icon.backgroundColor,
|
|
151
|
+
color: icon.fill,
|
|
152
|
+
} }
|
|
153
|
+
>
|
|
154
|
+
{ icon.node }
|
|
155
|
+
</div>
|
|
156
|
+
) }
|
|
157
|
+
</HStack>
|
|
158
|
+
<HStack alignment="edge">
|
|
159
|
+
<HStack expanded={ false } spacing="8px">
|
|
160
|
+
<Button
|
|
161
|
+
variant={ isConfigureState ? 'tertiary' : 'secondary' }
|
|
162
|
+
disabled={ isMuted && ! requirementsActionable }
|
|
163
|
+
onClick={ handleButtonClick }
|
|
164
|
+
size="compact"
|
|
165
|
+
>
|
|
166
|
+
{ buttonLabel }
|
|
167
|
+
</Button>
|
|
168
|
+
{ isConfigureState && !! moreControls?.length && (
|
|
169
|
+
<DropdownMenu
|
|
170
|
+
icon={ moreVertical }
|
|
171
|
+
label={ __( 'More', 'newspack-plugin' ) }
|
|
172
|
+
controls={ moreControls }
|
|
173
|
+
toggleProps={ { size: 'compact' } }
|
|
174
|
+
/>
|
|
175
|
+
) }
|
|
176
|
+
</HStack>
|
|
177
|
+
{ badge && <Badge text={ badge.text } level={ badge.level } /> }
|
|
178
|
+
</HStack>
|
|
179
|
+
</>
|
|
180
|
+
),
|
|
181
|
+
} }
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export default CardFeature;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CardFeature
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
@use "~@wordpress/base-styles/colors" as wp-colors;
|
|
6
|
+
@use "~@wordpress/base-styles/variables" as wp-vars;
|
|
7
|
+
|
|
8
|
+
.newspack-card-feature {
|
|
9
|
+
height: 100%;
|
|
10
|
+
|
|
11
|
+
.newspack-card--core__header {
|
|
12
|
+
height: 100%;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.newspack-card--core__header-content {
|
|
16
|
+
gap: 16px;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
height: 100%;
|
|
20
|
+
|
|
21
|
+
// Push the action row (buttons + badge) to the bottom.
|
|
22
|
+
> :last-child {
|
|
23
|
+
margin-top: auto;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&__content {
|
|
28
|
+
flex: 1;
|
|
29
|
+
min-width: 0; // prevent flex blowout on long words
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&__title {
|
|
33
|
+
font-size: wp-vars.$font-size-x-large;
|
|
34
|
+
line-height: wp-vars.$font-line-height-x-large;
|
|
35
|
+
text-align: left;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&__description {
|
|
39
|
+
text-align: left;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&__icon {
|
|
43
|
+
align-items: center;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex: 0 0 40px;
|
|
46
|
+
height: 40px;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
|
|
49
|
+
svg {
|
|
50
|
+
display: block;
|
|
51
|
+
fill: currentcolor;
|
|
52
|
+
height: 24px;
|
|
53
|
+
width: 24px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&--radius-small {
|
|
57
|
+
border-radius: wp-vars.$radius-small;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&--radius-full {
|
|
61
|
+
border-radius: wp-vars.$radius-round;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Unmet requirements state: lighter border, muted content.
|
|
66
|
+
&.newspack-card-feature--muted {
|
|
67
|
+
border-color: wp-colors.$gray-100;
|
|
68
|
+
box-shadow: wp-colors.$gray-100 0 0 0 1px;
|
|
69
|
+
|
|
70
|
+
.newspack-card-feature__title,
|
|
71
|
+
.newspack-card-feature__description {
|
|
72
|
+
color: wp-colors.$gray-700;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# CardForm
|
|
2
|
+
|
|
3
|
+
A card component for presenting a named setting or feature with an expandable inline form. When open, the card body reveals children (controls, fields, action buttons) and the header border is removed for a seamless look. Intended for lists of items that can each be independently enabled, edited, or configured without leaving the page.
|
|
4
|
+
|
|
5
|
+
## Layout rules
|
|
6
|
+
|
|
7
|
+
- Stack multiple `CardForm` cards inside a `<VStack>` — they are designed to appear as a list.
|
|
8
|
+
- The `actions` slot sits to the right of the badge. Keep it to one button; if you need multiple actions, use an `HStack` with `expanded={ false }`.
|
|
9
|
+
- The form body (`children`) is only mounted when `isOpen` is `true`.
|
|
10
|
+
|
|
11
|
+
## States
|
|
12
|
+
|
|
13
|
+
| State | `isOpen` | `badge` | `actions` example |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| **Disabled** | `false` | None | "Enable" (secondary) |
|
|
16
|
+
| **Enabling** | `true` | None | "Cancel" (tertiary) |
|
|
17
|
+
| **Enabled** | `false` | Success badge | "Edit" (tertiary) |
|
|
18
|
+
| **Editing** | `true` | Success badge | "Cancel" (tertiary) |
|
|
19
|
+
|
|
20
|
+
## Basic usage — enable/edit pattern
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { __ } from '@wordpress/i18n';
|
|
24
|
+
import { useState } from '@wordpress/element';
|
|
25
|
+
import { Button } from '@wordpress/components';
|
|
26
|
+
import { CardForm } from '../../../../../packages/components/src';
|
|
27
|
+
|
|
28
|
+
const [ isOpen, setIsOpen ] = useState( false );
|
|
29
|
+
const [ isEnabled, setIsEnabled ] = useState( false );
|
|
30
|
+
|
|
31
|
+
const handleClose = () => setIsOpen( false );
|
|
32
|
+
|
|
33
|
+
<CardForm
|
|
34
|
+
title={ __( 'Above Header', 'newspack-plugin' ) }
|
|
35
|
+
description={ __( 'Displays an ad above the site header.', 'newspack-plugin' ) }
|
|
36
|
+
badge={ isEnabled ? { level: 'success', text: __( 'Enabled', 'newspack-plugin' ) } : undefined }
|
|
37
|
+
actions={
|
|
38
|
+
isEnabled ? (
|
|
39
|
+
<Button variant="tertiary" size="compact" onClick={ () => isOpen ? handleClose() : setIsOpen( true ) }>
|
|
40
|
+
{ isOpen ? __( 'Cancel', 'newspack-plugin' ) : __( 'Edit', 'newspack-plugin' ) }
|
|
41
|
+
</Button>
|
|
42
|
+
) : (
|
|
43
|
+
<Button variant="secondary" size="compact" onClick={ () => setIsOpen( true ) }>
|
|
44
|
+
{ __( 'Enable', 'newspack-plugin' ) }
|
|
45
|
+
</Button>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
isOpen={ isOpen }
|
|
49
|
+
onRequestClose={ handleClose }
|
|
50
|
+
>
|
|
51
|
+
{ /* form controls */ }
|
|
52
|
+
<Button variant="primary" size="compact" onClick={ handleSave }>
|
|
53
|
+
{ __( 'Update', 'newspack-plugin' ) }
|
|
54
|
+
</Button>
|
|
55
|
+
</CardForm>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## With a custom badge level
|
|
59
|
+
|
|
60
|
+
The `badge` prop accepts any `BadgeLevel`. Use `warning` or `error` to communicate a degraded state.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<CardForm
|
|
64
|
+
title={ __( 'Above Header', 'newspack-plugin' ) }
|
|
65
|
+
badge={ { level: 'warning', text: __( 'Missing ad unit', 'newspack-plugin' ) } }
|
|
66
|
+
actions={ <Button variant="tertiary" size="compact">{ __( 'Edit', 'newspack-plugin' ) }</Button> }
|
|
67
|
+
isOpen={ false }
|
|
68
|
+
/>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Without a badge
|
|
72
|
+
|
|
73
|
+
Omit `badge` (or pass `undefined`) to show no badge at all.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<CardForm
|
|
77
|
+
title={ __( 'Sticky Footer', 'newspack-plugin' ) }
|
|
78
|
+
description={ __( 'Pins an ad to the bottom of the viewport.', 'newspack-plugin' ) }
|
|
79
|
+
actions={
|
|
80
|
+
<Button variant="secondary" size="compact" onClick={ handleEnable }>
|
|
81
|
+
{ __( 'Enable', 'newspack-plugin' ) }
|
|
82
|
+
</Button>
|
|
83
|
+
}
|
|
84
|
+
isOpen={ false }
|
|
85
|
+
/>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Props
|
|
89
|
+
|
|
90
|
+
| Prop | Type | Default | Description |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| `title` | `string` | — | Card heading (**required**) |
|
|
93
|
+
| `description` | `string` | — | Supporting text below the title |
|
|
94
|
+
| `badge` | `{ text: string; level?: BadgeLevel }` | — | Badge shown next to the actions slot. Omit or pass `undefined` to hide. |
|
|
95
|
+
| `actions` | `React.ReactNode` | — | JSX rendered in the header action area (buttons, dropdowns, etc.) |
|
|
96
|
+
| `isOpen` | `boolean` | `false` | When `true`, renders `children` in the card body and removes the header border |
|
|
97
|
+
| `onRequestClose` | `() => void` | — | Called when the user presses Escape while focus is inside the open form |
|
|
98
|
+
| `titleLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `3` | Heading level rendered for `title`. Pick the level that fits the surrounding document outline. |
|
|
99
|
+
| `className` | `string` | — | Additional class name applied to the card element |
|
|
100
|
+
| `children` | `React.ReactNode` | — | Form content rendered inside the card body when `isOpen` is `true` |
|
|
101
|
+
|
|
102
|
+
## Accessibility
|
|
103
|
+
|
|
104
|
+
- The body is rendered as a `role="region"` labelled by the title, so assistive tech announces it as a named region when focus enters.
|
|
105
|
+
- On open, focus moves to the first focusable element in the body (or to the region itself if none exist). On close, focus is restored to whatever was focused before opening — typically the trigger button.
|
|
106
|
+
- The Escape listener is scoped to the open form's body, so multiple open cards do not all close on a single keypress. If an inner control needs to consume Escape (for example, to close its own menu), call `event.preventDefault()` and CardForm will ignore it.
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
### `BadgeLevel`
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
type BadgeLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
|
113
|
+
```
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card Form component.
|
|
3
|
+
*
|
|
4
|
+
* A card with an expandable inline form — title, description, optional badge,
|
|
5
|
+
* and an actions slot in the header. When `isOpen` is true, children are
|
|
6
|
+
* rendered in the card body and the header border is removed for a seamless look.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* External dependencies
|
|
11
|
+
*/
|
|
12
|
+
import classnames from 'classnames';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* WordPress dependencies
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useRef, createElement } from '@wordpress/element';
|
|
18
|
+
import { useInstanceId } from '@wordpress/compose';
|
|
19
|
+
import { __experimentalHStack as HStack, __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Internal dependencies
|
|
23
|
+
*/
|
|
24
|
+
import Badge, { BadgeLevel } from '../badge';
|
|
25
|
+
import Card from '../card';
|
|
26
|
+
import './style.scss';
|
|
27
|
+
|
|
28
|
+
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
29
|
+
|
|
30
|
+
type CardFormProps = {
|
|
31
|
+
title: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
badge?: {
|
|
34
|
+
text: string;
|
|
35
|
+
level?: BadgeLevel;
|
|
36
|
+
};
|
|
37
|
+
/** JSX rendered in the header action area (buttons, etc.). */
|
|
38
|
+
actions?: React.ReactNode;
|
|
39
|
+
/** When true, children are shown and the header border is removed. */
|
|
40
|
+
isOpen?: boolean;
|
|
41
|
+
/** Called when the user presses Escape while the form is open. */
|
|
42
|
+
onRequestClose?: () => void;
|
|
43
|
+
/** Heading level for the title. Defaults to 3. */
|
|
44
|
+
titleLevel?: HeadingLevel;
|
|
45
|
+
className?: string;
|
|
46
|
+
children?: React.ReactNode;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const CardForm = ( { title, description, badge, actions, isOpen = false, onRequestClose, titleLevel = 3, className, children }: CardFormProps ) => {
|
|
50
|
+
const bodyRef = useRef< HTMLDivElement | null >( null );
|
|
51
|
+
const previousActiveRef = useRef< HTMLElement | null >( null );
|
|
52
|
+
const instanceId = useInstanceId( CardForm, 'newspack-card-form' );
|
|
53
|
+
const titleId = `${ instanceId }__title`;
|
|
54
|
+
const bodyId = `${ instanceId }__body`;
|
|
55
|
+
|
|
56
|
+
// Scope Escape handling to the open form's body, so multiple open CardForms
|
|
57
|
+
// don't all close on a single keypress, and callers can preventDefault from
|
|
58
|
+
// inner controls (e.g. select menus) without tripping the close.
|
|
59
|
+
useEffect( () => {
|
|
60
|
+
if ( ! isOpen || ! onRequestClose ) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const node = bodyRef.current;
|
|
64
|
+
if ( ! node ) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const handleKeyDown = ( event: KeyboardEvent ) => {
|
|
68
|
+
if ( event.key === 'Escape' && ! event.defaultPrevented ) {
|
|
69
|
+
onRequestClose();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
node.addEventListener( 'keydown', handleKeyDown );
|
|
73
|
+
return () => node.removeEventListener( 'keydown', handleKeyDown );
|
|
74
|
+
}, [ isOpen, onRequestClose ] );
|
|
75
|
+
|
|
76
|
+
// Move focus into the body on open and restore it to the trigger on close.
|
|
77
|
+
useEffect( () => {
|
|
78
|
+
if ( ! isOpen ) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const node = bodyRef.current;
|
|
82
|
+
if ( node ) {
|
|
83
|
+
previousActiveRef.current = ( node.ownerDocument?.activeElement ?? null ) as HTMLElement | null;
|
|
84
|
+
const focusable = node.querySelector< HTMLElement >(
|
|
85
|
+
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
86
|
+
);
|
|
87
|
+
( focusable ?? node ).focus();
|
|
88
|
+
}
|
|
89
|
+
return () => {
|
|
90
|
+
previousActiveRef.current?.focus?.();
|
|
91
|
+
};
|
|
92
|
+
}, [ isOpen ] );
|
|
93
|
+
|
|
94
|
+
const titleTag = `h${ titleLevel }`;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Card
|
|
98
|
+
className={ classnames( 'newspack-card-form', className, {
|
|
99
|
+
'newspack-card-form--open': isOpen,
|
|
100
|
+
} ) }
|
|
101
|
+
__experimentalCoreCard
|
|
102
|
+
isSmall
|
|
103
|
+
__experimentalCoreProps={ {
|
|
104
|
+
hasHeaderBorder: ! isOpen,
|
|
105
|
+
header: (
|
|
106
|
+
<HStack justify="space-between" style={ { width: '100%' } }>
|
|
107
|
+
<VStack spacing={ 0 } style={ { flex: 1, minWidth: 0 } }>
|
|
108
|
+
{ createElement( titleTag, { id: titleId, className: 'newspack-card-form__title' }, title ) }
|
|
109
|
+
{ description && <p className="newspack-card-form__description">{ description }</p> }
|
|
110
|
+
</VStack>
|
|
111
|
+
<HStack spacing={ 2 } expanded={ false }>
|
|
112
|
+
{ badge && <Badge text={ badge.text } level={ badge.level ?? 'success' } /> }
|
|
113
|
+
{ actions }
|
|
114
|
+
</HStack>
|
|
115
|
+
</HStack>
|
|
116
|
+
),
|
|
117
|
+
} }
|
|
118
|
+
>
|
|
119
|
+
{ isOpen && (
|
|
120
|
+
<div ref={ bodyRef } id={ bodyId } role="region" aria-labelledby={ titleId } tabIndex={ -1 } className="newspack-card-form__body">
|
|
121
|
+
{ children }
|
|
122
|
+
</div>
|
|
123
|
+
) }
|
|
124
|
+
</Card>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default CardForm;
|