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
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# StatCard
|
|
2
|
+
|
|
3
|
+
One figure presented as a scorecard: what it is, the number itself, and what the
|
|
4
|
+
number counts. Cards sit in a row, so they share a type scale and a null
|
|
5
|
+
treatment rather than each screen inventing its own.
|
|
6
|
+
|
|
7
|
+
The API is compound: a `StatCard.Root` and one subcomponent per slot. As `Drawer`
|
|
8
|
+
does, the parts hang off one exported object, which keeps a seven-part component
|
|
9
|
+
to one name on the barrel.
|
|
10
|
+
|
|
11
|
+
## Importing
|
|
12
|
+
|
|
13
|
+
The package barrel and the component's own entry point both work:
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
// The barrel.
|
|
17
|
+
import { StatCard } from 'newspack-components';
|
|
18
|
+
|
|
19
|
+
// The component on its own.
|
|
20
|
+
import StatCard from '../../packages/components/src/stat-card';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Both are safe. `Card.Root` from `@wordpress/ui` takes its background, border,
|
|
24
|
+
radius and padding from `--wpds-*` custom properties, which arrive with the
|
|
25
|
+
design-token sheet that `page/style.scss` imports, and that sheet rides in with
|
|
26
|
+
the barrel. But the CSS `@wordpress/ui` actually ships carries a fallback on
|
|
27
|
+
each of those properties, and the fallbacks are the same light-theme values the
|
|
28
|
+
token sheet sets, so a card outside the sheet renders the same chrome. Only a
|
|
29
|
+
consumer opting into non-default theme settings, a different corner radius say,
|
|
30
|
+
would see the two diverge.
|
|
31
|
+
|
|
32
|
+
The figure is unaffected either way: the card declares the Newspack accent on
|
|
33
|
+
itself rather than relying on the package's global remap. It declares the darker
|
|
34
|
+
and lighter steps with it, which its own rules never touch, so that a control in
|
|
35
|
+
a slot, a button in `suffix` or in `Footer`, takes the same accent as the figure
|
|
36
|
+
for its fill, its hover and its focus ring. Through the barrel those values are
|
|
37
|
+
already global and the block changes nothing; it earns its keep on the
|
|
38
|
+
deep-import route the rest of this package recommends.
|
|
39
|
+
|
|
40
|
+
The exported prop types travel with neither route. The barrel is a `.js` file so
|
|
41
|
+
it cannot re-export types, and the package ships no declarations (it compiles
|
|
42
|
+
with Babel and sets no `types` field), so `StatCardRootProps` and its siblings
|
|
43
|
+
are reachable only through a path import into `src/stat-card` from inside this
|
|
44
|
+
monorepo.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
import { __ } from '@wordpress/i18n';
|
|
50
|
+
import { StatCard } from 'newspack-components';
|
|
51
|
+
|
|
52
|
+
<StatCard.Root>
|
|
53
|
+
<StatCard.Label>{ __( 'Subscribers reached', 'newspack-plugin' ) }</StatCard.Label>
|
|
54
|
+
<StatCard.Body>
|
|
55
|
+
<StatCard.Value value="1,284" />
|
|
56
|
+
</StatCard.Body>
|
|
57
|
+
<StatCard.Footer>
|
|
58
|
+
{ __( 'Readers who received at least one campaign this month.', 'newspack-plugin' ) }
|
|
59
|
+
</StatCard.Footer>
|
|
60
|
+
</StatCard.Root>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every slot except `Root` is optional. `Body` is what pins `Footer` to the bottom
|
|
64
|
+
of the card, so a row of cards with descriptions of different lengths still has
|
|
65
|
+
its numbers on one line.
|
|
66
|
+
|
|
67
|
+
## The figure is the caller's to format
|
|
68
|
+
|
|
69
|
+
`StatCard.Value` takes a string or a number, not an element. Currency symbols,
|
|
70
|
+
thousands separators, percentages, abbreviated millions and locale all belong to
|
|
71
|
+
the screen that knows what the figure means; the component only sizes it.
|
|
72
|
+
|
|
73
|
+
The one thing it does own is the absence of a figure. Pass `value={ null }` and
|
|
74
|
+
it renders the null glyph, standing in for "there is no number here" as opposed
|
|
75
|
+
to a zero that genuinely is one. `undefined` and a blank string take the same
|
|
76
|
+
path, so `value={ data?.count }` is safe before the data arrives, and a field
|
|
77
|
+
that reports "no data" as `""`, or as a padded sentinel, gets the glyph rather
|
|
78
|
+
than an empty hero.
|
|
79
|
+
|
|
80
|
+
A zero is a figure and renders as one. That distinction is the whole reason the
|
|
81
|
+
glyph exists, so nothing in the component may treat `0` as missing.
|
|
82
|
+
|
|
83
|
+
## Scale and the container query
|
|
84
|
+
|
|
85
|
+
The hero figure is `clamp( 20px, 14cqi, 48px )`, against a
|
|
86
|
+
`container-type: inline-size` on `Root`. A four-figure number in a narrow column
|
|
87
|
+
shrinks to fit rather than overflowing or forcing a smaller fixed size on every
|
|
88
|
+
card in the row, and the floor stops it shrinking under its own label.
|
|
89
|
+
|
|
90
|
+
The ceiling and its ratio are `$font-size-3x-large` and
|
|
91
|
+
`$font-line-height-3x-large` in the package's `src/_variables.scss`, which
|
|
92
|
+
carries the steps this package needs above the `@wordpress/base-styles` scale.
|
|
93
|
+
The line height is unitless on purpose: the font size is fluid, so a fixed
|
|
94
|
+
value from the base-styles pairs would drift out of proportion as the figure
|
|
95
|
+
shrinks.
|
|
96
|
+
|
|
97
|
+
That query is why the parts insist on a `Root`: a `StatCard.Value` rendered
|
|
98
|
+
loose would size against whichever container it happened to land in, which fails
|
|
99
|
+
quietly and looks like a styling bug.
|
|
100
|
+
|
|
101
|
+
Inline-size containment has a second consequence: the card contributes nothing
|
|
102
|
+
to its own intrinsic width, so **the parent layout has to give `Root` a definite
|
|
103
|
+
inline size**. A grid track or a `flex: 1` item is fine. Dropped somewhere its
|
|
104
|
+
width would come from its contents, such as an `inline-block` or a table cell,
|
|
105
|
+
it collapses to nothing. Equal widths across a row are what keep one type scale
|
|
106
|
+
across that row.
|
|
107
|
+
|
|
108
|
+
It also makes the card a containing block for `position: absolute` and
|
|
109
|
+
`position: fixed` descendants, and `Card.Root` clips its overflow. Anything
|
|
110
|
+
positioned that renders inline inside the card, such as a popover on a control
|
|
111
|
+
in `suffix`, is therefore trapped by the card unless it portals out.
|
|
112
|
+
`InfoButton` portals to the shared overlay slot and the tooltips and popovers in
|
|
113
|
+
`@wordpress/components` portal by default, so this mostly matters if a consumer
|
|
114
|
+
registers its own `Popover.Slot` inside a card.
|
|
115
|
+
|
|
116
|
+
For a hero that is a phrase rather than a number ("0 of 17", "No conversions"),
|
|
117
|
+
pass `variant="text"`. It keeps the slot and drops the display scale, which
|
|
118
|
+
would otherwise wrap a sentence across three lines.
|
|
119
|
+
|
|
120
|
+
## Naming the figure to screen readers
|
|
121
|
+
|
|
122
|
+
A visible figure whose meaning rests on punctuation or a glyph needs saying
|
|
123
|
+
differently out loud. `valueLabel` replaces the spoken text: the visible span
|
|
124
|
+
goes `aria-hidden`, and the label follows in a `VisuallyHidden` from
|
|
125
|
+
`@wordpress/ui`, which brings its own CSS. The card asks nothing of the host
|
|
126
|
+
page for that, where wp-admin's `.screen-reader-text` would have made every
|
|
127
|
+
consumer's stylesheet part of the contract.
|
|
128
|
+
|
|
129
|
+
This is deliberately not `role="img"` with an `aria-label`. ARIA prohibits
|
|
130
|
+
naming a generic element, so the label needs a role to survive, and `img` makes
|
|
131
|
+
NVDA and VoiceOver announce "graphic" for what is a typographic placeholder.
|
|
132
|
+
Hiding the glyph and supplying real text avoids both.
|
|
133
|
+
|
|
134
|
+
The null glyph gets "Not applicable" by default, or whatever `labels` on the
|
|
135
|
+
`Root` puts in its place. Pass `valueLabel` to say something more specific, e.g.
|
|
136
|
+
why the figure is missing. An empty or blank `valueLabel` falls back to that
|
|
137
|
+
default rather than hiding the figure behind a name that says nothing.
|
|
138
|
+
|
|
139
|
+
### Outside `newspack-plugin`
|
|
140
|
+
|
|
141
|
+
The spoken defaults, "Not applicable", "Up" and "Down", carry the
|
|
142
|
+
`newspack-plugin` text domain, as every string in this package does. WordPress
|
|
143
|
+
resolves JS translations per script handle, so a bundle registered against
|
|
144
|
+
another domain never loads them and they read in English.
|
|
145
|
+
|
|
146
|
+
`labels` on `StatCard.Root` replaces all three at once, from whichever domain the
|
|
147
|
+
consumer is registered under:
|
|
148
|
+
|
|
149
|
+
```jsx
|
|
150
|
+
<StatCard.Root
|
|
151
|
+
labels={ {
|
|
152
|
+
notApplicable: __( 'Not applicable', 'newspack-manager' ),
|
|
153
|
+
up: __( 'Up', 'newspack-manager' ),
|
|
154
|
+
down: __( 'Down', 'newspack-manager' ),
|
|
155
|
+
} }
|
|
156
|
+
>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A wrapper component that renders the `Root`, which is how both adopters use this,
|
|
160
|
+
sets it once and every card underneath is right. `valueLabel`, `directionLabel`
|
|
161
|
+
and `label` are unchanged: they remain the per-instance override for one figure
|
|
162
|
+
or one change that needs saying differently, and they still win. A blank entry in
|
|
163
|
+
`labels` falls back to the built-in default, so a translation that came back
|
|
164
|
+
empty cannot leave the glyph or the arrow unnamed.
|
|
165
|
+
|
|
166
|
+
## Anatomy, not policy
|
|
167
|
+
|
|
168
|
+
The component is the chrome, the layout and the type scale. Anything that
|
|
169
|
+
decides *what* to show is the consumer's.
|
|
170
|
+
|
|
171
|
+
That line matters most for Insights, whose `MetricCard` wraps this one and adds
|
|
172
|
+
period-over-period deltas, warming states, "not configured" overlays and
|
|
173
|
+
zero-fallback heroes. None of that belongs here; a rule about a Google Analytics
|
|
174
|
+
property is not a rule about a card. `MetricCard` composes the slots and keeps
|
|
175
|
+
its own props.
|
|
176
|
+
|
|
177
|
+
## Refs and pass-through props
|
|
178
|
+
|
|
179
|
+
Every part forwards a ref to the element it renders and passes any prop its own
|
|
180
|
+
table does not name straight through to that element, so `id`, `style`, `title`
|
|
181
|
+
and `data-*` all land on the DOM node:
|
|
182
|
+
|
|
183
|
+
| Part | The element it renders |
|
|
184
|
+
|------|------------------------|
|
|
185
|
+
| `Root` | The card |
|
|
186
|
+
| `Label` | The label row, not the heading |
|
|
187
|
+
| `Body` | The body column |
|
|
188
|
+
| `Value` | The figure, not the row it shares with a `suffix` |
|
|
189
|
+
| `Delta` | The delta |
|
|
190
|
+
| `Secondary` | The secondary line |
|
|
191
|
+
| `Footer` | The footer column |
|
|
192
|
+
|
|
193
|
+
That is what lets a wrapper hang the unabbreviated amount off a `$1.2M` without
|
|
194
|
+
wrapping the figure in an element the body layout would then have to carry.
|
|
195
|
+
|
|
196
|
+
## Class names
|
|
197
|
+
|
|
198
|
+
The component emits these, so a stylesheet can hook onto any of them:
|
|
199
|
+
|
|
200
|
+
| Class | Element |
|
|
201
|
+
|-------|---------|
|
|
202
|
+
| `newspack-stat-card` | The card |
|
|
203
|
+
| `newspack-stat-card__content` | The content column |
|
|
204
|
+
| `newspack-stat-card__label` | The label row |
|
|
205
|
+
| `newspack-stat-card__label-text` | The heading inside that row |
|
|
206
|
+
| `newspack-stat-card__body` | The body column |
|
|
207
|
+
| `newspack-stat-card__figure` | The row the figure shares with a `Value` `suffix`, present only when there is one |
|
|
208
|
+
| `newspack-stat-card__value` | The figure |
|
|
209
|
+
| `newspack-stat-card__delta` | The delta |
|
|
210
|
+
| `newspack-stat-card__secondary` | The secondary line |
|
|
211
|
+
| `newspack-stat-card__footer` | The footer column |
|
|
212
|
+
| `newspack-stat-card__description` | Each paragraph of the description |
|
|
213
|
+
|
|
214
|
+
`newspack-stat-card__action` is the one exception, and it runs the other way: the card
|
|
215
|
+
carries the rule but never applies the class, so an action in `Footer` takes it by hand.
|
|
216
|
+
|
|
217
|
+
## `StatCard.Root`
|
|
218
|
+
|
|
219
|
+
| Prop | Type | Default | Description |
|
|
220
|
+
|------|------|---------|-------------|
|
|
221
|
+
| `children` | `React.ReactNode` | — | The slots. |
|
|
222
|
+
| `className` | `string` | — | Merged onto the card, alongside `newspack-stat-card`. |
|
|
223
|
+
| `heading` | `2`–`6` | `3` | Heading level for `StatCard.Label`, passed through context. |
|
|
224
|
+
| `labels` | `{ notApplicable, up, down }` | The built-in strings | Spoken defaults for every card underneath. See [Outside `newspack-plugin`](#outside-newspack-plugin). |
|
|
225
|
+
|
|
226
|
+
Renders `Card.Root` / `Card.Content` from `@wordpress/ui`, and owns the
|
|
227
|
+
container query.
|
|
228
|
+
|
|
229
|
+
## `StatCard.Label`
|
|
230
|
+
|
|
231
|
+
| Prop | Type | Default | Description |
|
|
232
|
+
|------|------|---------|-------------|
|
|
233
|
+
| `children` | `React.ReactNode` | — | The label text. |
|
|
234
|
+
| `className` | `string` | — | Merged onto the label row, not the heading. |
|
|
235
|
+
| `heading` | `2`–`6` | Root's | Overrides the level set on `Root`. |
|
|
236
|
+
| `suffix` | `React.ReactNode` | — | Rendered beside the heading, e.g. an info button. |
|
|
237
|
+
|
|
238
|
+
`suffix` sits next to the heading rather than inside it, so a control there stays
|
|
239
|
+
out of the document outline and off the heading's accessible name. Supplementary
|
|
240
|
+
context belongs in an `InfoButton`, which already carries the popup, the touch
|
|
241
|
+
behaviour and the accessible name; the slot itself takes anything.
|
|
242
|
+
|
|
243
|
+
The card pulls an `InfoButton` in that slot back to the 20px line
|
|
244
|
+
`heading-large()` gives the heading, so a card carrying one and a card without
|
|
245
|
+
still have their figures level. The button makes no assumption about its host, so
|
|
246
|
+
the trim lives here rather than on it.
|
|
247
|
+
|
|
248
|
+
```jsx
|
|
249
|
+
<StatCard.Label
|
|
250
|
+
suffix={
|
|
251
|
+
<InfoButton
|
|
252
|
+
description={ __( 'Averaged across the timeframe.', 'newspack-plugin' ) }
|
|
253
|
+
triggerLabel={ __( 'More information about Average order value', 'newspack-plugin' ) }
|
|
254
|
+
/>
|
|
255
|
+
}
|
|
256
|
+
>
|
|
257
|
+
{ __( 'Average order value', 'newspack-plugin' ) }
|
|
258
|
+
</StatCard.Label>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Any other control in that slot has to hold the 20px line itself, or it grows the
|
|
262
|
+
row and drops that card's figure below the rest.
|
|
263
|
+
|
|
264
|
+
A level outside 2–6 falls back to `3` and warns outside production, rather than
|
|
265
|
+
rendering an element that is not a heading at all.
|
|
266
|
+
|
|
267
|
+
## `StatCard.Body`
|
|
268
|
+
|
|
269
|
+
| Prop | Type | Default | Description |
|
|
270
|
+
|------|------|---------|-------------|
|
|
271
|
+
| `children` | `React.ReactNode` | — | The value, plus anything that belongs with it. |
|
|
272
|
+
| `className` | `string` | — | Merged onto the body. |
|
|
273
|
+
|
|
274
|
+
A column that takes the free space. Put `StatCard.Value` in it, plus a
|
|
275
|
+
`StatCard.Secondary` line or a consumer-owned element such as a delta.
|
|
276
|
+
|
|
277
|
+
## `StatCard.Value`
|
|
278
|
+
|
|
279
|
+
| Prop | Type | Default | Description |
|
|
280
|
+
|------|------|---------|-------------|
|
|
281
|
+
| `className` | `string` | — | Merged onto the value. |
|
|
282
|
+
| `suffix` | `React.ReactNode` | — | Rendered in a row beside the figure, e.g. a `StatCard.Delta`. |
|
|
283
|
+
| `value` | `string` \| `number` \| `null` \| `undefined` | — | **Required.** Pre-formatted. `null`, `undefined` and a blank string render the null glyph. |
|
|
284
|
+
| `valueLabel` | `string` | Root's `notApplicable` when null | Spoken instead of the visible value. |
|
|
285
|
+
| `variant` | `'figure'` \| `'text'` | `'figure'` | `text` drops the hero scale for a phrase. |
|
|
286
|
+
|
|
287
|
+
With a `suffix`, the figure and the suffix share a baseline-aligned row. Without
|
|
288
|
+
one, the figure renders on its own with no extra wrapper.
|
|
289
|
+
|
|
290
|
+
## `StatCard.Delta`
|
|
291
|
+
|
|
292
|
+
| Prop | Type | Default | Description |
|
|
293
|
+
|------|------|---------|-------------|
|
|
294
|
+
| `children` | `React.ReactNode` | — | The change, pre-formatted. Must be non-interactive. |
|
|
295
|
+
| `className` | `string` | — | Merged onto the delta. |
|
|
296
|
+
| `direction` | `'up'` \| `'down'` | — | **Required.** Which arrow to show. |
|
|
297
|
+
| `directionLabel` | `string` | Root's `up` or `down` | Spoken in place of the direction. |
|
|
298
|
+
| `label` | `string` | — | Spoken in place of the whole delta. Wins over `directionLabel`. |
|
|
299
|
+
| `tone` | `'positive'` \| `'negative'` \| `'neutral'` | `'neutral'` | Which colour to use. |
|
|
300
|
+
|
|
301
|
+
```jsx
|
|
302
|
+
<StatCard.Value
|
|
303
|
+
value="1,284"
|
|
304
|
+
suffix={ <StatCard.Delta direction="up" tone="positive">2%</StatCard.Delta> }
|
|
305
|
+
/>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**`direction` and `tone` are deliberately separate.** A rise is not always good
|
|
309
|
+
news: a refund rate climbing 2% wants an up arrow and a negative tone. The
|
|
310
|
+
component owns the arrow, the size and the colour; the caller, which is the only
|
|
311
|
+
one that knows what the figure means, decides which of them applies.
|
|
312
|
+
|
|
313
|
+
The arrow is `aria-hidden` and its meaning supplied as text, so the delta reads
|
|
314
|
+
as "Up 2%" rather than as a glyph. That also means the direction survives for
|
|
315
|
+
anyone who cannot use the colour, which the colour alone would not.
|
|
316
|
+
|
|
317
|
+
The tone does not survive it. "Up 2%" reads the same whether the rise is good
|
|
318
|
+
news or bad, because that difference lives only in the colour. Where it matters,
|
|
319
|
+
put it in words: `label` replaces the whole spoken delta, and the arrow and the
|
|
320
|
+
change are hidden behind it.
|
|
321
|
+
|
|
322
|
+
That hiding is why the children must be text rather than a control. Anything
|
|
323
|
+
focusable there would still take tab focus while being hidden from the
|
|
324
|
+
accessibility tree, which is a state a screen-reader user cannot make sense of.
|
|
325
|
+
|
|
326
|
+
```jsx
|
|
327
|
+
<StatCard.Delta
|
|
328
|
+
direction="up"
|
|
329
|
+
tone="negative"
|
|
330
|
+
label={ sprintf(
|
|
331
|
+
// translators: %s is the change, e.g. "2%".
|
|
332
|
+
__( '%s more refunds than last month', 'newspack-plugin' ),
|
|
333
|
+
'2%'
|
|
334
|
+
) }
|
|
335
|
+
>
|
|
336
|
+
2%
|
|
337
|
+
</StatCard.Delta>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
A blank `label` or `directionLabel` counts as none, and falls back the way an
|
|
341
|
+
empty one does, so a delta is never left announcing whitespace.
|
|
342
|
+
|
|
343
|
+
`label` is also the way out of the default's word order. "Up 2%" is the arrow's
|
|
344
|
+
text followed by the children, which the markup fixes: a language that wants the
|
|
345
|
+
figure inside the phrase, or the direction after it, cannot get there by swapping
|
|
346
|
+
one word with `directionLabel`. One translatable sentence can.
|
|
347
|
+
|
|
348
|
+
A direction outside `up` and `down` shows no arrow and warns outside production.
|
|
349
|
+
With nothing else to go on it says nothing, rather than naming the opposite
|
|
350
|
+
direction. A `directionLabel` or a `label` is still spoken, because the caller
|
|
351
|
+
chose those words.
|
|
352
|
+
|
|
353
|
+
## `StatCard.Secondary`
|
|
354
|
+
|
|
355
|
+
| Prop | Type | Default | Description |
|
|
356
|
+
|------|------|---------|-------------|
|
|
357
|
+
| `children` | `React.ReactNode` | — | A short qualifying line under the value. |
|
|
358
|
+
| `className` | `string` | — | Merged onto the line. |
|
|
359
|
+
|
|
360
|
+
It takes the figure's colour and a heading scale, so it reads as part of the
|
|
361
|
+
headline rather than a note under it. The quiet line is the footer's description.
|
|
362
|
+
|
|
363
|
+
## `StatCard.Footer`
|
|
364
|
+
|
|
365
|
+
| Prop | Type | Default | Description |
|
|
366
|
+
|------|------|---------|-------------|
|
|
367
|
+
| `children` | `React.ReactNode` | — | The description, plus any action. |
|
|
368
|
+
| `className` | `string` | — | Merged onto the footer. |
|
|
369
|
+
|
|
370
|
+
Pinned to the bottom. A run of text children shares one `<p>` carrying the
|
|
371
|
+
description styling, so `<StatCard.Footer>Applies to { count } products</StatCard.Footer>`
|
|
372
|
+
is one sentence rather than three stacked lines; elements pass through
|
|
373
|
+
untouched, which is how an action lands under the text.
|
|
374
|
+
|
|
375
|
+
An element ends the run, so a description with inline markup in the middle of it
|
|
376
|
+
would be split across several blocks. A Fragment does not: `createInterpolateElement`
|
|
377
|
+
returns one, and the sentence inside it stays part of the run, styled like any
|
|
378
|
+
other description.
|
|
379
|
+
|
|
380
|
+
```jsx
|
|
381
|
+
<StatCard.Footer>
|
|
382
|
+
{ createInterpolateElement( __( 'Applies to <b>12</b> products.', 'newspack-plugin' ), {
|
|
383
|
+
b: <strong />,
|
|
384
|
+
} ) }
|
|
385
|
+
</StatCard.Footer>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Anything else you want kept together, wrap yourself and it passes through as one:
|
|
389
|
+
|
|
390
|
+
```jsx
|
|
391
|
+
<StatCard.Footer>
|
|
392
|
+
<p className="newspack-stat-card__description">
|
|
393
|
+
Applies to <strong>12</strong> products.
|
|
394
|
+
</p>
|
|
395
|
+
</StatCard.Footer>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
That rule keys on the wrapper rather than on what it holds, so a Fragment is
|
|
399
|
+
folded into the run whatever is inside it. A group of non-text content, two
|
|
400
|
+
buttons say, needs a real element around it in the same way: inside a Fragment
|
|
401
|
+
both would land in the description `<p>` and take its quiet 12px type, and
|
|
402
|
+
anything rendering a `<div>` there would trip React's nesting warning as well.
|
|
403
|
+
|
|
404
|
+
An action keeps the description's type scale by taking the
|
|
405
|
+
`newspack-stat-card__action` class:
|
|
406
|
+
|
|
407
|
+
```jsx
|
|
408
|
+
<StatCard.Footer>
|
|
409
|
+
{ __( 'Products this rule applies to.', 'newspack-plugin' ) }
|
|
410
|
+
<Button isLink className="newspack-stat-card__action" onClick={ onView }>
|
|
411
|
+
{ __( 'See the products', 'newspack-plugin' ) }
|
|
412
|
+
</Button>
|
|
413
|
+
</StatCard.Footer>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## `STAT_CARD_NULL_GLYPH`
|
|
417
|
+
|
|
418
|
+
The glyph `StatCard.Value` shows for `null`, exported so a table under a row of
|
|
419
|
+
cards can show the same one:
|
|
420
|
+
|
|
421
|
+
```jsx
|
|
422
|
+
import { STAT_CARD_NULL_GLYPH } from 'newspack-components';
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Outside the Root
|
|
426
|
+
|
|
427
|
+
Every subcomponent reads Root's context. Outside one it throws "StatCard
|
|
428
|
+
subcomponents must be rendered inside StatCard.Root.", which is what surfaces the
|
|
429
|
+
mistake in development and in tests.
|
|
430
|
+
|
|
431
|
+
A production build warns and falls back to the default context instead. Nothing
|
|
432
|
+
in this package or in `newspack-plugin` puts an error boundary above these cards,
|
|
433
|
+
so throwing there would blank an admin screen; a figure sized against the wrong
|
|
434
|
+
container is the smaller failure of the two.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { forwardRef } from '@wordpress/element';
|
|
5
|
+
import { Stack } from '@wordpress/ui';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* External dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import classnames from 'classnames';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies.
|
|
14
|
+
*/
|
|
15
|
+
import { useStatCardContext } from './context';
|
|
16
|
+
import type { StatCardBodyProps } from './types';
|
|
17
|
+
|
|
18
|
+
const Body = forwardRef< HTMLDivElement, StatCardBodyProps >( function Body( { className, children, ...props }, ref ) {
|
|
19
|
+
useStatCardContext();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Stack ref={ ref } direction="column" gap="xs" className={ classnames( 'newspack-stat-card__body', className ) } { ...props }>
|
|
23
|
+
{ children }
|
|
24
|
+
</Stack>
|
|
25
|
+
);
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
export default Body;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { createContext, useContext, useEffect } from '@wordpress/element';
|
|
5
|
+
import { _x } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import type { StatCardHeadingLevel, StatCardLabels } from './types';
|
|
11
|
+
|
|
12
|
+
type StatCardContextValue = {
|
|
13
|
+
heading: StatCardHeadingLevel;
|
|
14
|
+
labels: StatCardLabels;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ORPHAN_MESSAGE = 'StatCard subcomponents must be rendered inside StatCard.Root.';
|
|
18
|
+
|
|
19
|
+
// Resolved per call rather than once at module load, so a locale switched after
|
|
20
|
+
// the bundle evaluates still reaches the defaults. A blank override is a missing
|
|
21
|
+
// one: the glyph and the arrow must never be left announcing nothing.
|
|
22
|
+
export const resolveStatCardLabels = ( labels?: Partial< StatCardLabels > ): StatCardLabels => ( {
|
|
23
|
+
notApplicable: labels?.notApplicable?.trim() || _x( 'Not applicable', 'a statistic with no number to show', 'newspack-plugin' ),
|
|
24
|
+
up: labels?.up?.trim() || _x( 'Up', 'a statistic that has increased', 'newspack-plugin' ),
|
|
25
|
+
down: labels?.down?.trim() || _x( 'Down', 'a statistic that has decreased', 'newspack-plugin' ),
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
export const StatCardContext = createContext< StatCardContextValue | null >( null );
|
|
29
|
+
|
|
30
|
+
export const useStatCardContext = (): StatCardContextValue => {
|
|
31
|
+
const context = useContext( StatCardContext );
|
|
32
|
+
const isOrphan = ! context;
|
|
33
|
+
|
|
34
|
+
useEffect( () => {
|
|
35
|
+
if ( ! isOrphan ) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
console.warn( ORPHAN_MESSAGE );
|
|
40
|
+
}, [ isOrphan ] );
|
|
41
|
+
|
|
42
|
+
if ( ! context ) {
|
|
43
|
+
// A loose slot sizes its figure against the wrong container, which is
|
|
44
|
+
// cosmetic. Nothing above these cards catches an error, so throwing in
|
|
45
|
+
// production would blank an admin screen over it.
|
|
46
|
+
if ( 'production' !== process.env.NODE_ENV ) {
|
|
47
|
+
throw new Error( ORPHAN_MESSAGE );
|
|
48
|
+
}
|
|
49
|
+
return { heading: 3, labels: resolveStatCardLabels() };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return context;
|
|
53
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { forwardRef, useEffect } from '@wordpress/element';
|
|
5
|
+
import { VisuallyHidden } from '@wordpress/ui';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* External dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import classnames from 'classnames';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies.
|
|
14
|
+
*/
|
|
15
|
+
import { useStatCardContext } from './context';
|
|
16
|
+
import type { StatCardDeltaDirection, StatCardDeltaProps, StatCardDeltaTone } from './types';
|
|
17
|
+
|
|
18
|
+
const glyphs: Record< StatCardDeltaDirection, string > = {
|
|
19
|
+
up: '↑',
|
|
20
|
+
down: '↓',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const tones: StatCardDeltaTone[] = [ 'positive', 'negative', 'neutral' ];
|
|
24
|
+
|
|
25
|
+
const Delta = forwardRef< HTMLSpanElement, StatCardDeltaProps >( function Delta(
|
|
26
|
+
{ direction, tone = 'neutral', directionLabel, label, className, children, ...props },
|
|
27
|
+
ref
|
|
28
|
+
) {
|
|
29
|
+
const { labels } = useStatCardContext();
|
|
30
|
+
|
|
31
|
+
useEffect( () => {
|
|
32
|
+
if ( 'production' === process.env.NODE_ENV ) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if ( ! glyphs[ direction ] ) {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.warn( `StatCard.Delta: unknown direction "${ direction }". Use one of ${ Object.keys( glyphs ).join( ', ' ) }.` );
|
|
38
|
+
}
|
|
39
|
+
if ( ! tones.includes( tone ) ) {
|
|
40
|
+
// eslint-disable-next-line no-console
|
|
41
|
+
console.warn( `StatCard.Delta: unknown tone "${ tone }", falling back to neutral. Use one of ${ tones.join( ', ' ) }.` );
|
|
42
|
+
}
|
|
43
|
+
}, [ direction, tone ] );
|
|
44
|
+
|
|
45
|
+
const directions: Record< StatCardDeltaDirection, string > = {
|
|
46
|
+
up: labels.up,
|
|
47
|
+
down: labels.down,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const glyph = glyphs[ direction ];
|
|
51
|
+
// Trimmed, and `||` not `??`: a blank label is a missing one. The fallback
|
|
52
|
+
// is keyed on the arrow's own direction, so an unrecognised one goes
|
|
53
|
+
// unspoken rather than announced as its opposite; words the caller wrote
|
|
54
|
+
// are honoured either way.
|
|
55
|
+
const named = label?.trim();
|
|
56
|
+
const spoken = directionLabel?.trim() || directions[ direction ];
|
|
57
|
+
|
|
58
|
+
const classes = classnames(
|
|
59
|
+
'newspack-stat-card__delta',
|
|
60
|
+
'neutral' !== tone && tones.includes( tone ) && `newspack-stat-card__delta--${ tone }`,
|
|
61
|
+
className
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<span ref={ ref } className={ classes } { ...props }>
|
|
66
|
+
{ named ? (
|
|
67
|
+
<>
|
|
68
|
+
{ /* `label` names the whole delta, so the change it restates is hidden with the arrow. */ }
|
|
69
|
+
<span aria-hidden="true">
|
|
70
|
+
{ glyph }
|
|
71
|
+
{ children }
|
|
72
|
+
</span>
|
|
73
|
+
<VisuallyHidden render={ <span /> }>{ named }</VisuallyHidden>
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
<>
|
|
77
|
+
{ /* The arrow is hidden and its meaning given as text, since a bare glyph announces inconsistently. */ }
|
|
78
|
+
{ glyph && <span aria-hidden="true">{ glyph }</span> }
|
|
79
|
+
{ /* The trailing space separates the direction from the change in the raw text, not only in the layout. */ }
|
|
80
|
+
{ spoken && <VisuallyHidden render={ <span /> }>{ `${ spoken } ` }</VisuallyHidden> }
|
|
81
|
+
{ children }
|
|
82
|
+
</>
|
|
83
|
+
) }
|
|
84
|
+
</span>
|
|
85
|
+
);
|
|
86
|
+
} );
|
|
87
|
+
|
|
88
|
+
export default Delta;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { Children, forwardRef, Fragment, isValidElement } from '@wordpress/element';
|
|
5
|
+
import { Stack } from '@wordpress/ui';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* External dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import classnames from 'classnames';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies.
|
|
14
|
+
*/
|
|
15
|
+
import { useStatCardContext } from './context';
|
|
16
|
+
import type { StatCardFooterProps } from './types';
|
|
17
|
+
|
|
18
|
+
// A run of text shares one wrapper, so `Applies to { count } products` is one
|
|
19
|
+
// sentence rather than three stacked paragraphs.
|
|
20
|
+
const asParts = ( children: React.ReactNode ) => {
|
|
21
|
+
const parts: React.ReactNode[] = [];
|
|
22
|
+
let text: React.ReactNode[] = [];
|
|
23
|
+
|
|
24
|
+
const flushText = () => {
|
|
25
|
+
if ( text.some( part => '' !== String( part ).trim() ) ) {
|
|
26
|
+
parts.push(
|
|
27
|
+
<p key={ `text-${ parts.length }` } className="newspack-stat-card__description">
|
|
28
|
+
{ text }
|
|
29
|
+
</p>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
text = [];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
Children.toArray( children ).forEach( child => {
|
|
36
|
+
// A Fragment is part of the run, not a block of its own:
|
|
37
|
+
// `createInterpolateElement` returns one, and the sentence it holds
|
|
38
|
+
// belongs in the description wrapper like any other text.
|
|
39
|
+
if ( isValidElement( child ) && Fragment !== child.type ) {
|
|
40
|
+
flushText();
|
|
41
|
+
parts.push( child );
|
|
42
|
+
} else {
|
|
43
|
+
text.push( child );
|
|
44
|
+
}
|
|
45
|
+
} );
|
|
46
|
+
flushText();
|
|
47
|
+
|
|
48
|
+
return parts;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const Footer = forwardRef< HTMLDivElement, StatCardFooterProps >( function Footer( { className, children, ...props }, ref ) {
|
|
52
|
+
useStatCardContext();
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Stack
|
|
56
|
+
ref={ ref }
|
|
57
|
+
direction="column"
|
|
58
|
+
align="flex-start"
|
|
59
|
+
gap="xs"
|
|
60
|
+
className={ classnames( 'newspack-stat-card__footer', className ) }
|
|
61
|
+
{ ...props }
|
|
62
|
+
>
|
|
63
|
+
{ asParts( children ) }
|
|
64
|
+
</Stack>
|
|
65
|
+
);
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
export default Footer;
|