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.
Files changed (251) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/DEVELOPMENT.md +42 -21
  3. package/dist/cjs/_mixins.scss +13 -0
  4. package/dist/cjs/_variables.scss +12 -0
  5. package/dist/cjs/action-card/index.js +18 -10
  6. package/dist/cjs/action-card/index.test.js +91 -0
  7. package/dist/cjs/action-card/style.scss +2 -40
  8. package/dist/cjs/button/index.js +8 -3
  9. package/dist/cjs/button/index.test.js +61 -5
  10. package/dist/cjs/card/core-card.js +0 -2
  11. package/dist/cjs/card/style-core.scss +7 -5
  12. package/dist/cjs/card-feature/index.js +75 -61
  13. package/dist/cjs/card-feature/index.test.js +396 -54
  14. package/dist/cjs/card-feature/style.scss +20 -31
  15. package/dist/cjs/card-form/index.js +5 -5
  16. package/dist/cjs/card-sortable-list/index.js +5 -3
  17. package/dist/cjs/card-sortable-list/index.test.js +50 -0
  18. package/dist/cjs/collapsible-group/context.js +19 -0
  19. package/dist/cjs/collapsible-group/index.js +24 -0
  20. package/dist/cjs/collapsible-group/index.test.js +215 -0
  21. package/dist/cjs/collapsible-group/item.js +61 -0
  22. package/dist/cjs/collapsible-group/root.js +62 -0
  23. package/dist/cjs/collapsible-group/style.scss +47 -0
  24. package/dist/cjs/collapsible-group/types.js +5 -0
  25. package/dist/cjs/empty-state/actions.js +45 -0
  26. package/dist/cjs/empty-state/context.js +41 -0
  27. package/dist/cjs/empty-state/header.js +66 -0
  28. package/dist/cjs/empty-state/index.js +31 -0
  29. package/dist/cjs/empty-state/index.test.js +303 -0
  30. package/dist/cjs/empty-state/root.js +56 -0
  31. package/dist/cjs/empty-state/style.scss +51 -0
  32. package/dist/cjs/empty-state/types.js +5 -0
  33. package/dist/cjs/index.js +34 -20
  34. package/dist/cjs/info-button/index.js +59 -43
  35. package/dist/cjs/info-button/index.test.js +199 -0
  36. package/dist/cjs/info-button/style.scss +41 -9
  37. package/dist/cjs/modal/style.scss +12 -12
  38. package/dist/cjs/newspack-icon/style.scss +4 -4
  39. package/dist/cjs/notice/style.scss +3 -3
  40. package/dist/cjs/page/style.scss +16 -16
  41. package/dist/cjs/plugin-installer/index.js +5 -9
  42. package/dist/cjs/plugin-installer/style.scss +3 -14
  43. package/dist/cjs/section-header/index.js +31 -18
  44. package/dist/cjs/section-header/index.test.js +40 -0
  45. package/dist/cjs/section-header/style.scss +22 -6
  46. package/dist/cjs/settings/DateRangeSetting.js +309 -0
  47. package/dist/cjs/settings/SettingsSection.js +19 -5
  48. package/dist/cjs/settings/SettingsSection.test.js +49 -0
  49. package/dist/cjs/settings/index.js +3 -1
  50. package/dist/cjs/settings/style.scss +40 -7
  51. package/dist/cjs/stat-card/body.js +41 -0
  52. package/dist/cjs/stat-card/constants.js +11 -0
  53. package/dist/cjs/stat-card/context.js +54 -0
  54. package/dist/cjs/stat-card/delta.js +91 -0
  55. package/dist/cjs/stat-card/footer.js +72 -0
  56. package/dist/cjs/stat-card/index.js +47 -0
  57. package/dist/cjs/stat-card/index.test.js +938 -0
  58. package/dist/cjs/stat-card/label.js +66 -0
  59. package/dist/cjs/stat-card/root.js +69 -0
  60. package/dist/cjs/stat-card/secondary.js +38 -0
  61. package/dist/cjs/stat-card/style.scss +89 -0
  62. package/dist/cjs/stat-card/types.js +5 -0
  63. package/dist/cjs/stat-card/value.js +78 -0
  64. package/dist/cjs/status-indicator/index.js +66 -0
  65. package/dist/cjs/status-indicator/index.test.js +101 -0
  66. package/dist/cjs/status-indicator/statuses.js +39 -0
  67. package/dist/cjs/status-indicator/style.scss +9 -0
  68. package/dist/cjs/status-indicator/types.js +5 -0
  69. package/dist/cjs/style.scss +9 -9
  70. package/dist/cjs/tabbed-navigation/style.scss +6 -4
  71. package/dist/cjs/types.js +5 -0
  72. package/dist/cjs/web-preview/style.scss +11 -11
  73. package/dist/cjs/with-wizard/style.scss +10 -10
  74. package/dist/cjs/with-wizard-screen/style.scss +9 -9
  75. package/dist/cjs/wizard/index.js +33 -4
  76. package/dist/cjs/wizard/store/index.js +23 -1
  77. package/dist/esm/_mixins.scss +13 -0
  78. package/dist/esm/_variables.scss +12 -0
  79. package/dist/esm/action-card/index.js +18 -10
  80. package/dist/esm/action-card/index.test.js +89 -0
  81. package/dist/esm/action-card/style.scss +2 -40
  82. package/dist/esm/button/index.js +9 -4
  83. package/dist/esm/button/index.test.js +63 -5
  84. package/dist/esm/card/core-card.js +0 -2
  85. package/dist/esm/card/style-core.scss +7 -5
  86. package/dist/esm/card-feature/index.js +78 -62
  87. package/dist/esm/card-feature/index.test.js +397 -55
  88. package/dist/esm/card-feature/style.scss +20 -31
  89. package/dist/esm/card-form/index.js +5 -5
  90. package/dist/esm/card-sortable-list/index.js +6 -4
  91. package/dist/esm/card-sortable-list/index.test.js +46 -0
  92. package/dist/esm/collapsible-group/context.js +13 -0
  93. package/dist/esm/collapsible-group/index.js +17 -0
  94. package/dist/esm/collapsible-group/index.test.js +215 -0
  95. package/dist/esm/collapsible-group/item.js +53 -0
  96. package/dist/esm/collapsible-group/root.js +54 -0
  97. package/dist/esm/collapsible-group/style.scss +47 -0
  98. package/dist/esm/collapsible-group/types.js +1 -0
  99. package/dist/esm/empty-state/actions.js +37 -0
  100. package/dist/esm/empty-state/context.js +35 -0
  101. package/dist/esm/empty-state/header.js +58 -0
  102. package/dist/esm/empty-state/index.js +23 -0
  103. package/dist/esm/empty-state/index.test.js +299 -0
  104. package/dist/esm/empty-state/root.js +56 -0
  105. package/dist/esm/empty-state/style.scss +51 -0
  106. package/dist/esm/empty-state/types.js +1 -0
  107. package/dist/esm/index.js +4 -2
  108. package/dist/esm/info-button/index.js +54 -43
  109. package/dist/esm/info-button/index.test.js +195 -0
  110. package/dist/esm/info-button/style.scss +41 -9
  111. package/dist/esm/modal/style.scss +12 -12
  112. package/dist/esm/newspack-icon/style.scss +4 -4
  113. package/dist/esm/notice/style.scss +3 -3
  114. package/dist/esm/page/style.scss +16 -16
  115. package/dist/esm/plugin-installer/index.js +5 -9
  116. package/dist/esm/plugin-installer/style.scss +3 -14
  117. package/dist/esm/section-header/index.js +27 -14
  118. package/dist/esm/section-header/index.test.js +36 -0
  119. package/dist/esm/section-header/style.scss +22 -6
  120. package/dist/esm/settings/DateRangeSetting.js +304 -0
  121. package/dist/esm/settings/SettingsSection.js +16 -3
  122. package/dist/esm/settings/SettingsSection.test.js +45 -0
  123. package/dist/esm/settings/index.js +3 -1
  124. package/dist/esm/settings/style.scss +40 -7
  125. package/dist/esm/stat-card/body.js +36 -0
  126. package/dist/esm/stat-card/constants.js +5 -0
  127. package/dist/esm/stat-card/context.js +48 -0
  128. package/dist/esm/stat-card/delta.js +86 -0
  129. package/dist/esm/stat-card/footer.js +67 -0
  130. package/dist/esm/stat-card/index.js +33 -0
  131. package/dist/esm/stat-card/index.test.js +939 -0
  132. package/dist/esm/stat-card/label.js +61 -0
  133. package/dist/esm/stat-card/root.js +64 -0
  134. package/dist/esm/stat-card/secondary.js +33 -0
  135. package/dist/esm/stat-card/style.scss +89 -0
  136. package/dist/esm/stat-card/types.js +1 -0
  137. package/dist/esm/stat-card/value.js +73 -0
  138. package/dist/esm/status-indicator/index.js +52 -0
  139. package/dist/esm/status-indicator/index.test.js +96 -0
  140. package/dist/esm/status-indicator/statuses.js +33 -0
  141. package/dist/esm/status-indicator/style.scss +9 -0
  142. package/dist/esm/status-indicator/types.js +1 -0
  143. package/dist/esm/style.scss +9 -9
  144. package/dist/esm/tabbed-navigation/style.scss +6 -4
  145. package/dist/esm/types.js +1 -0
  146. package/dist/esm/web-preview/style.scss +11 -11
  147. package/dist/esm/with-wizard/style.scss +10 -10
  148. package/dist/esm/with-wizard-screen/style.scss +9 -9
  149. package/dist/esm/wizard/index.js +38 -7
  150. package/dist/esm/wizard/store/index.js +23 -1
  151. package/package.json +1 -1
  152. package/src/_mixins.scss +13 -0
  153. package/src/_variables.scss +12 -0
  154. package/src/action-card/action-card.d.ts +3 -2
  155. package/src/action-card/index.js +12 -12
  156. package/src/action-card/index.test.js +56 -0
  157. package/src/action-card/style.scss +2 -40
  158. package/src/button/index.test.js +52 -5
  159. package/src/button/index.tsx +9 -4
  160. package/src/card/core-card.js +0 -2
  161. package/src/card/style-core.scss +7 -5
  162. package/src/card-feature/README.md +68 -27
  163. package/src/card-feature/index.test.js +242 -27
  164. package/src/card-feature/index.tsx +77 -67
  165. package/src/card-feature/style.scss +20 -31
  166. package/src/card-form/README.md +10 -10
  167. package/src/card-form/index.tsx +4 -8
  168. package/src/card-sortable-list/README.md +4 -5
  169. package/src/card-sortable-list/index.test.js +29 -0
  170. package/src/card-sortable-list/index.tsx +7 -4
  171. package/src/collapsible-group/README.md +50 -0
  172. package/src/collapsible-group/context.ts +13 -0
  173. package/src/collapsible-group/index.test.js +168 -0
  174. package/src/collapsible-group/index.tsx +17 -0
  175. package/src/collapsible-group/item.tsx +42 -0
  176. package/src/collapsible-group/root.tsx +42 -0
  177. package/src/collapsible-group/style.scss +47 -0
  178. package/src/collapsible-group/types.ts +22 -0
  179. package/src/drawer/README.md +2 -2
  180. package/src/empty-state/README.md +165 -0
  181. package/src/empty-state/actions.tsx +37 -0
  182. package/src/empty-state/context.ts +40 -0
  183. package/src/empty-state/header.tsx +48 -0
  184. package/src/empty-state/index.test.js +247 -0
  185. package/src/empty-state/index.tsx +22 -0
  186. package/src/empty-state/root.tsx +39 -0
  187. package/src/empty-state/style.scss +51 -0
  188. package/src/empty-state/types.ts +33 -0
  189. package/src/index.js +4 -2
  190. package/src/info-button/README.md +115 -0
  191. package/src/info-button/index.test.js +163 -0
  192. package/src/info-button/index.tsx +64 -0
  193. package/src/info-button/style.scss +41 -9
  194. package/src/modal/style.scss +12 -12
  195. package/src/newspack-icon/style.scss +4 -4
  196. package/src/notice/style.scss +3 -3
  197. package/src/page/style.scss +16 -16
  198. package/src/plugin-installer/index.js +3 -9
  199. package/src/plugin-installer/style.scss +3 -14
  200. package/src/section-header/index.js +27 -14
  201. package/src/section-header/index.test.js +28 -0
  202. package/src/section-header/style.scss +22 -6
  203. package/src/settings/DateRangeSetting.js +266 -0
  204. package/src/settings/SettingsSection.js +24 -4
  205. package/src/settings/SettingsSection.test.js +45 -0
  206. package/src/settings/index.js +2 -0
  207. package/src/settings/style.scss +40 -7
  208. package/src/stat-card/README.md +434 -0
  209. package/src/stat-card/body.tsx +28 -0
  210. package/src/stat-card/constants.ts +5 -0
  211. package/src/stat-card/context.ts +53 -0
  212. package/src/stat-card/delta.tsx +88 -0
  213. package/src/stat-card/footer.tsx +68 -0
  214. package/src/stat-card/index.test.js +818 -0
  215. package/src/stat-card/index.tsx +48 -0
  216. package/src/stat-card/label.tsx +61 -0
  217. package/src/stat-card/root.tsx +40 -0
  218. package/src/stat-card/secondary.tsx +27 -0
  219. package/src/stat-card/style.scss +89 -0
  220. package/src/stat-card/types.ts +84 -0
  221. package/src/stat-card/value.tsx +72 -0
  222. package/src/status-indicator/README.md +123 -0
  223. package/src/status-indicator/index.test.js +70 -0
  224. package/src/status-indicator/index.tsx +39 -0
  225. package/src/status-indicator/statuses.ts +35 -0
  226. package/src/status-indicator/style.scss +9 -0
  227. package/src/status-indicator/types.ts +28 -0
  228. package/src/style.scss +9 -9
  229. package/src/tabbed-navigation/style.scss +6 -4
  230. package/src/types.ts +25 -0
  231. package/src/web-preview/style.scss +11 -11
  232. package/src/with-wizard/style.scss +10 -10
  233. package/src/with-wizard-screen/style.scss +9 -9
  234. package/src/wizard/index.js +49 -7
  235. package/src/wizard/store/index.js +26 -1
  236. package/dist/cjs/accordion/index.js +0 -72
  237. package/dist/cjs/accordion/index.test.js +0 -80
  238. package/dist/cjs/accordion/style.scss +0 -35
  239. package/dist/cjs/badge/index.js +0 -32
  240. package/dist/cjs/badge/style.scss +0 -36
  241. package/dist/esm/accordion/index.js +0 -63
  242. package/dist/esm/accordion/index.test.js +0 -79
  243. package/dist/esm/accordion/style.scss +0 -35
  244. package/dist/esm/badge/index.js +0 -24
  245. package/dist/esm/badge/style.scss +0 -36
  246. package/src/accordion/index.js +0 -46
  247. package/src/accordion/index.test.js +0 -62
  248. package/src/accordion/style.scss +0 -35
  249. package/src/badge/index.tsx +0 -26
  250. package/src/badge/style.scss +0 -36
  251. package/src/info-button/index.js +0 -38
@@ -0,0 +1,304 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
3
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
4
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
5
+ var _excluded = ["start", "end", "onChange", "label"];
6
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
7
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
8
+ /**
9
+ * WordPress dependencies.
10
+ */
11
+ import { Fragment, useEffect, useState } from '@wordpress/element';
12
+ import { __, _n, sprintf } from '@wordpress/i18n';
13
+
14
+ /**
15
+ * Internal dependencies.
16
+ */
17
+ import { SelectControl, TextControl } from '../';
18
+
19
+ /**
20
+ * A bound is stored as { type: 'absolute', date } or { type: 'relative', days },
21
+ * where days is negative for the past and positive for the future. The UI splits
22
+ * the relative case in two so a publisher never has to type a minus sign.
23
+ */
24
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
+ var BOUND_TYPES = [{
26
+ label: __('Any', 'newspack-plugin'),
27
+ value: ''
28
+ }, {
29
+ label: __('Date', 'newspack-plugin'),
30
+ value: 'absolute'
31
+ }, {
32
+ label: __('Days ago', 'newspack-plugin'),
33
+ value: 'past'
34
+ }, {
35
+ label: __('Days from now', 'newspack-plugin'),
36
+ value: 'future'
37
+ }];
38
+
39
+ // Upper magnitude for a relative bound: ~273 years, far inside Date's range,
40
+ // and it keeps the resolved year four digits so the matcher's lexicographic
41
+ // compare stays valid. Import and REST bypass this input, so the matcher still
42
+ // fails closed on larger offsets — this just turns a wild typo into
43
+ // browser-level validation instead of a segment that matches nobody.
44
+ var MAX_RELATIVE_DAYS = 100000;
45
+
46
+ // Today as YYYY-MM-DD in local time. toISOString() converts to UTC and would give
47
+ // the wrong day for anyone west of Greenwich.
48
+ var today = function today() {
49
+ var date = new Date();
50
+ var month = String(date.getMonth() + 1).padStart(2, '0');
51
+ var day = String(date.getDate()).padStart(2, '0');
52
+ return "".concat(date.getFullYear(), "-").concat(month, "-").concat(day);
53
+ };
54
+
55
+ // Zero days is the same day either way, and normalizes to "Days ago".
56
+ var boundTypeOf = function boundTypeOf(bound) {
57
+ if (!bound || !bound.type) {
58
+ return '';
59
+ }
60
+ if ('absolute' === bound.type) {
61
+ return 'absolute';
62
+ }
63
+ return bound.days > 0 ? 'future' : 'past';
64
+ };
65
+ var boundValueOf = function boundValueOf(bound) {
66
+ if (!bound) {
67
+ return '';
68
+ }
69
+ return 'absolute' === bound.type ? bound.date || '' : String(Math.abs(bound.days || 0));
70
+ };
71
+
72
+ // `seed` is set only when the publisher picks a bound type, so a newly chosen
73
+ // bound starts on a usable value instead of being half-filled. It must stay off
74
+ // for edits to the value itself: a date input reports '' both when cleared and
75
+ // transiently while its value is being retyped, and substituting today() there
76
+ // would silently move a saved window out from under the publisher.
77
+ var makeBound = function makeBound(type, rawValue) {
78
+ var seed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
79
+ if ('absolute' === type) {
80
+ var date = rawValue || (seed ? today() : '');
81
+ // An absolute bound with no date is not a bound: emitted and saved, it
82
+ // would reach the matcher as a bound it can only reject, silently
83
+ // shrinking the segment to nobody. The row itself stays open — the
84
+ // control falls back to the chosen bound type while the bound is absent.
85
+ return date ? {
86
+ type: 'absolute',
87
+ date: date
88
+ } : undefined;
89
+ }
90
+ // A cleared magnitude mirrors a cleared date: no bound at all, not a
91
+ // zero-day bound — { days: 0 } would quietly move the window's edge to
92
+ // today and snap the emptied input back to '0' mid-retype. An explicit
93
+ // '0' is falsy-but-present, so the check is on emptiness, not truthiness.
94
+ var raw = '' === rawValue && seed ? '0' : rawValue;
95
+ if ('' === raw) {
96
+ return undefined;
97
+ }
98
+ // Number() rather than parseInt(): a number input accepts scientific
99
+ // notation, and parseInt( '1e2' ) reads it as 1 instead of 100.
100
+ var days = Math.abs(Math.trunc(Number(raw)) || 0);
101
+ return {
102
+ type: 'relative',
103
+ days: 'future' === type ? days : -days
104
+ };
105
+ };
106
+ var DateRangeBound = function DateRangeBound(_ref) {
107
+ var label = _ref.label,
108
+ testId = _ref.testId,
109
+ bound = _ref.bound,
110
+ _onChange = _ref.onChange;
111
+ // The bound type is normally derived from the stored value, but a zero-magnitude
112
+ // relative bound (`days: 0`) is the same day whether the publisher chose "ago" or
113
+ // "from now" — the sign can't tell them apart. `chosenType` records the direction
114
+ // to fall back on while the bound stays ambiguous, and starts out `null` — no
115
+ // direction known yet — since the bound this control receives can arrive
116
+ // asynchronously (e.g. an existing segment fetched after first mount).
117
+ var _useState = useState(null),
118
+ _useState2 = _slicedToArray(_useState, 2),
119
+ chosenType = _useState2[0],
120
+ setChosenType = _useState2[1];
121
+ var derivedType = boundTypeOf(bound);
122
+ var isAmbiguous = 'relative' === (bound === null || bound === void 0 ? void 0 : bound.type) && 0 === bound.days;
123
+
124
+ // Keep chosenType in step with any unambiguous bound — freshly chosen by the
125
+ // publisher in this control, or loaded from storage — so the direction survives
126
+ // if the value is later cleared down to an ambiguous zero. A bound of '' (no
127
+ // bound at all, e.g. before an async load resolves) doesn't establish a
128
+ // direction; only a real, unambiguous one does.
129
+ useEffect(function () {
130
+ if ('' !== derivedType && !isAmbiguous) {
131
+ setChosenType(derivedType);
132
+ }
133
+ }, [derivedType, isAmbiguous]);
134
+
135
+ // Also fall back while there is no bound at all: a cleared date emits no
136
+ // bound (see makeBound), and deriving from the absent value would collapse
137
+ // the row to "Any" mid-edit instead of leaving the empty input for retyping.
138
+ var type = null !== chosenType && (isAmbiguous || !bound) ? chosenType : derivedType;
139
+ return /*#__PURE__*/_jsxs("div", {
140
+ className: "newspack-settings__date-range-bound",
141
+ children: [/*#__PURE__*/_jsx(SelectControl, {
142
+ label: label
143
+ // Without this the select renders at 32px while the sibling input is
144
+ // 40px, so the two boxes in a row don't match.
145
+ ,
146
+ __next40pxDefaultSize: true,
147
+ value: type,
148
+ options: BOUND_TYPES,
149
+ onChange: function onChange(nextType) {
150
+ setChosenType(nextType);
151
+ _onChange(nextType ? makeBound(nextType, '', true) : undefined);
152
+ }
153
+ }), '' !== type && /*#__PURE__*/_jsx(TextControl, {
154
+ "data-testid": testId
155
+ // The visible label sits on the sibling select, so without one here
156
+ // the input has no accessible name at all. Composed with the row
157
+ // label so the two bound rows' inputs stay distinguishable — two
158
+ // bare "Days" spin buttons announce identically.
159
+ ,
160
+ label: 'absolute' === type ?
161
+ // translators: %s: the bound row label (From/To).
162
+ sprintf(__('%s date', 'newspack-plugin'), label) :
163
+ // translators: %s: the bound row label (From/To).
164
+ sprintf(__('%s days', 'newspack-plugin'), label),
165
+ hideLabelFromVision: true,
166
+ type: 'absolute' === type ? 'date' : 'number',
167
+ min: 'absolute' === type ? undefined : 0,
168
+ max: 'absolute' === type ? undefined : MAX_RELATIVE_DAYS,
169
+ value: boundValueOf(bound),
170
+ onChange: function onChange(nextValue) {
171
+ return _onChange(makeBound(type, nextValue));
172
+ }
173
+ })]
174
+ });
175
+ };
176
+
177
+ /**
178
+ * Rolling windows offered as one-click presets. Each is the trailing window
179
+ * ending today, i.e. `start: -N days`, `end: today` — the same shape the custom
180
+ * rows produce, so a preset is a shortcut rather than a separate stored format.
181
+ */
182
+ var PRESET_DAYS = [7, 30, 365];
183
+ var PRESETS = [{
184
+ label: __('Any', 'newspack-plugin'),
185
+ value: ''
186
+ }].concat(_toConsumableArray(PRESET_DAYS.map(function (days) {
187
+ return {
188
+ label: sprintf(_n('Last %d day', 'Last %d days', days, 'newspack-plugin'), days),
189
+ value: String(days)
190
+ };
191
+ })), [{
192
+ label: __('Custom', 'newspack-plugin'),
193
+ value: 'custom'
194
+ }]);
195
+
196
+ /**
197
+ * Which preset a stored range corresponds to.
198
+ *
199
+ * @param {*} start The start bound.
200
+ * @param {*} end The end bound.
201
+ * @return {string} A PRESETS value: '' when unbounded, the day count when the
202
+ * range is exactly one of the presets, otherwise 'custom'.
203
+ */
204
+ var presetOf = function presetOf(start, end) {
205
+ if (!start && !end) {
206
+ return '';
207
+ }
208
+ if (start && end && 'relative' === start.type && 'relative' === end.type && 0 === end.days && start.days < 0 && PRESET_DAYS.includes(Math.abs(start.days))) {
209
+ return String(Math.abs(start.days));
210
+ }
211
+ return 'custom';
212
+ };
213
+ var DateRangeSetting = function DateRangeSetting(_ref2) {
214
+ var start = _ref2.start,
215
+ end = _ref2.end,
216
+ onChange = _ref2.onChange,
217
+ label = _ref2.label,
218
+ props = _objectWithoutProperties(_ref2, _excluded);
219
+ var derivedPreset = presetOf(start, end);
220
+ // A range that matches a preset is indistinguishable from the same range built
221
+ // by hand, and an empty one is indistinguishable from "Custom, nothing filled
222
+ // in yet" — so a deliberate Custom choice has to be remembered rather than
223
+ // re-derived. Starts null: nothing chosen yet, so the stored value decides.
224
+ var _useState3 = useState(null),
225
+ _useState4 = _slicedToArray(_useState3, 2),
226
+ chosenPreset = _useState4[0],
227
+ setChosenPreset = _useState4[1];
228
+
229
+ // Custom explains any range, so it is never overridden by what the value looks
230
+ // like. Every other selection stays in step with a range that arrived from
231
+ // storage or changed elsewhere.
232
+ useEffect(function () {
233
+ setChosenPreset(function (chosen) {
234
+ return 'custom' === chosen ? chosen : derivedPreset;
235
+ });
236
+ }, [derivedPreset]);
237
+ var preset = null === chosenPreset ? derivedPreset : chosenPreset;
238
+
239
+ // Both keys are always emitted: the consumer merges the value into the stored
240
+ // criterion, so omitting one would leave a stale bound behind. An empty range
241
+ // emits nothing at all, which drops the criterion — an empty `{}` would reach
242
+ // the matcher as "no bounds to check" and match every reader with a date.
243
+ var changeRange = function changeRange(nextStart, nextEnd) {
244
+ return onChange(nextStart || nextEnd ? {
245
+ start: nextStart,
246
+ end: nextEnd
247
+ } : undefined);
248
+ };
249
+ var changePreset = function changePreset(next) {
250
+ setChosenPreset(next);
251
+ if ('custom' === next) {
252
+ // Keep the current range so the rows open on what the preset meant.
253
+ return;
254
+ }
255
+ if ('' === next) {
256
+ onChange(undefined);
257
+ return;
258
+ }
259
+ changeRange({
260
+ type: 'relative',
261
+ days: -Number(next)
262
+ }, {
263
+ type: 'relative',
264
+ days: 0
265
+ });
266
+ };
267
+ return /*#__PURE__*/_jsxs("div", _objectSpread(_objectSpread({}, props), {}, {
268
+ children: [/*#__PURE__*/_jsx("div", {
269
+ className: "newspack-settings__date-range-preset",
270
+ children: /*#__PURE__*/_jsx(SelectControl
271
+ // The criterion's own name is the visible heading of the section
272
+ // this renders into, so a visible label here would just repeat it —
273
+ // but the heading is a bare span with no label association, so the
274
+ // consumer-passed name is what keeps several date criteria's preset
275
+ // selectors distinguishable to a screen reader.
276
+ , {
277
+ label: label || __('Date range', 'newspack-plugin'),
278
+ hideLabelFromVision: true,
279
+ __next40pxDefaultSize: true,
280
+ "data-testid": "date-range-preset",
281
+ value: preset,
282
+ options: PRESETS,
283
+ onChange: changePreset
284
+ })
285
+ }), 'custom' === preset && /*#__PURE__*/_jsxs(Fragment, {
286
+ children: [/*#__PURE__*/_jsx(DateRangeBound, {
287
+ label: __('From', 'newspack-plugin'),
288
+ testId: "date-range-start-value",
289
+ bound: start,
290
+ onChange: function onChange(nextStart) {
291
+ return changeRange(nextStart, end);
292
+ }
293
+ }), /*#__PURE__*/_jsx(DateRangeBound, {
294
+ label: __('To', 'newspack-plugin'),
295
+ testId: "date-range-end-value",
296
+ bound: end,
297
+ onChange: function onChange(nextEnd) {
298
+ return changeRange(start, nextEnd);
299
+ }
300
+ })]
301
+ })]
302
+ }));
303
+ };
304
+ export default DateRangeSetting;
@@ -1,7 +1,14 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import { __, sprintf } from '@wordpress/i18n';
5
+ import { Stack } from '@wordpress/ui';
6
+
1
7
  /**
2
8
  * Internal dependencies.
3
9
  */
4
- import { Grid, InfoButton } from '../';
10
+ import Grid from '../grid';
11
+ import InfoButton from '../info-button';
5
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
13
  var SettingSection = function SettingSection(_ref) {
7
14
  var title = _ref.title,
@@ -11,12 +18,18 @@ var SettingSection = function SettingSection(_ref) {
11
18
  columns: 1,
12
19
  gutter: 8,
13
20
  className: "newspack-settings__section",
14
- children: [/*#__PURE__*/_jsxs("div", {
21
+ children: [/*#__PURE__*/_jsxs(Stack, {
22
+ direction: "row",
23
+ align: "flex-start",
24
+ gap: "xs",
15
25
  className: "newspack-settings__section__title",
16
26
  children: [/*#__PURE__*/_jsx("span", {
17
27
  children: title
18
28
  }), description && /*#__PURE__*/_jsx(InfoButton, {
19
- text: description
29
+ description: description,
30
+ triggerLabel: typeof title === 'string' ? sprintf(
31
+ // translators: %s is the name of the setting being explained.
32
+ __('More information about %s', 'newspack-plugin'), title) : __('More information', 'newspack-plugin')
20
33
  })]
21
34
  }), /*#__PURE__*/_jsx("div", {
22
35
  className: "newspack-settings__section__content",
@@ -0,0 +1,45 @@
1
+ /**
2
+ * External dependencies.
3
+ */
4
+ import { fireEvent, render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies.
8
+ */
9
+ import SettingsSection from './SettingsSection';
10
+ import { jsx as _jsx } from "react/jsx-runtime";
11
+ var DESCRIPTION = 'Number of articles read in the last 30 day period.';
12
+ describe('SettingsSection', function () {
13
+ it('reveals the description from the info trigger', function () {
14
+ render(/*#__PURE__*/_jsx(SettingsSection, {
15
+ title: "Articles read",
16
+ description: DESCRIPTION,
17
+ children: /*#__PURE__*/_jsx("input", {})
18
+ }));
19
+ expect(screen.queryByText(DESCRIPTION)).not.toBeInTheDocument();
20
+ fireEvent.click(screen.getByRole('button', {
21
+ name: 'More information about Articles read'
22
+ }));
23
+ expect(screen.getByText(DESCRIPTION)).toBeInTheDocument();
24
+ });
25
+ it('falls back to a plain trigger name when the title is not a string', function () {
26
+ render(/*#__PURE__*/_jsx(SettingsSection, {
27
+ title: /*#__PURE__*/_jsx("em", {
28
+ children: "Articles read"
29
+ }),
30
+ description: DESCRIPTION,
31
+ children: /*#__PURE__*/_jsx("input", {})
32
+ }));
33
+ expect(screen.getByRole('button', {
34
+ name: 'More information'
35
+ })).toBeInTheDocument();
36
+ });
37
+ it('renders no info trigger without a description', function () {
38
+ render(/*#__PURE__*/_jsx(SettingsSection, {
39
+ title: "Articles read",
40
+ children: /*#__PURE__*/_jsx("input", {})
41
+ }));
42
+ expect(screen.getByText('Articles read')).toBeInTheDocument();
43
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
44
+ });
45
+ });
@@ -1,9 +1,11 @@
1
1
  import SettingsCard from './SettingsCard';
2
2
  import SettingsSection from './SettingsSection';
3
3
  import MinMaxSetting from './MinMaxSetting';
4
+ import DateRangeSetting from './DateRangeSetting';
4
5
  import './style.scss';
5
6
  export default {
6
7
  SettingsCard: SettingsCard,
7
8
  SettingsSection: SettingsSection,
8
- MinMaxSetting: MinMaxSetting
9
+ MinMaxSetting: MinMaxSetting,
10
+ DateRangeSetting: DateRangeSetting
9
11
  };
@@ -1,3 +1,5 @@
1
+ @use "~@wordpress/base-styles/variables" as wp-vars;
2
+
1
3
  .newspack-settings {
2
4
  &__card {
3
5
  &.newspack-card.newspack-action-card {
@@ -28,17 +30,18 @@
28
30
 
29
31
  .newspack-settings__section {
30
32
  &__title {
31
- align-items: center;
32
- display: flex;
33
- margin: 0;
34
-
35
33
  span {
36
- line-height: 24px;
34
+ line-height: wp-vars.$font-line-height-small;
35
+ }
36
+
37
+ .newspack-info-button {
38
+ margin-block: calc((#{wp-vars.$font-line-height-small} - #{wp-vars.$button-size-small}) / 2);
37
39
  }
38
40
  }
39
41
  &__content {
40
42
  > div,
41
- .newspack-settings__min-max {
43
+ .newspack-settings__min-max,
44
+ .newspack-settings__date-range-bound {
42
45
  margin: 0;
43
46
  display: flex;
44
47
  align-items: center;
@@ -49,7 +52,19 @@
49
52
  }
50
53
  }
51
54
  .newspack-autocomplete-tokenfield,
52
- .newspack-settings__min-max {
55
+ .newspack-settings__min-max,
56
+ .newspack-settings__date-range-bound {
57
+ width: 100%;
58
+ }
59
+ .newspack-settings__date-range-bound {
60
+ // Overrides the `center` above, which has to be beaten at this
61
+ // nesting level. The select carries the visible From/To label and
62
+ // the value input's is hidden, so the two controls differ in
63
+ // overall height; centering floats the input up against the label,
64
+ // while bottom-aligning lines the two boxes up with each other.
65
+ align-items: flex-end;
66
+ }
67
+ .newspack-settings__date-range-preset {
53
68
  width: 100%;
54
69
  }
55
70
  .newspack-autocomplete-tokenfield {
@@ -90,6 +105,24 @@
90
105
  margin-top: 16px !important;
91
106
  }
92
107
  }
108
+ .newspack-settings__date-range-preset + .newspack-settings__date-range-bound {
109
+ margin-top: 16px !important;
110
+ }
111
+ .newspack-settings__date-range-bound {
112
+ gap: 8px;
113
+
114
+ .newspack-select-control {
115
+ flex: 0 0 auto;
116
+ }
117
+
118
+ .newspack-text-control {
119
+ flex: 1 1 auto;
120
+ }
121
+
122
+ + .newspack-settings__date-range-bound {
123
+ margin-top: 16px !important;
124
+ }
125
+ }
93
126
  }
94
127
 
95
128
  &__no-border {
@@ -0,0 +1,36 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
3
+ var _excluded = ["className", "children"];
4
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
5
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
6
+ /**
7
+ * WordPress dependencies.
8
+ */
9
+ import { forwardRef } from '@wordpress/element';
10
+ import { Stack } from '@wordpress/ui';
11
+
12
+ /**
13
+ * External dependencies.
14
+ */
15
+ import classnames from 'classnames';
16
+
17
+ /**
18
+ * Internal dependencies.
19
+ */
20
+ import { useStatCardContext } from './context';
21
+ import { jsx as _jsx } from "react/jsx-runtime";
22
+ var Body = forwardRef(function Body(_ref, ref) {
23
+ var className = _ref.className,
24
+ children = _ref.children,
25
+ props = _objectWithoutProperties(_ref, _excluded);
26
+ useStatCardContext();
27
+ return /*#__PURE__*/_jsx(Stack, _objectSpread(_objectSpread({
28
+ ref: ref,
29
+ direction: "column",
30
+ gap: "xs",
31
+ className: classnames('newspack-stat-card__body', className)
32
+ }, props), {}, {
33
+ children: children
34
+ }));
35
+ });
36
+ export default Body;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Stands in for a figure that does not exist, rather than a zero that does.
3
+ * Exported so tables can show the same glyph as the cards above them.
4
+ */
5
+ export var STAT_CARD_NULL_GLYPH = '—';
@@ -0,0 +1,48 @@
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
+
11
+ var ORPHAN_MESSAGE = 'StatCard subcomponents must be rendered inside StatCard.Root.';
12
+
13
+ // Resolved per call rather than once at module load, so a locale switched after
14
+ // the bundle evaluates still reaches the defaults. A blank override is a missing
15
+ // one: the glyph and the arrow must never be left announcing nothing.
16
+ export var resolveStatCardLabels = function resolveStatCardLabels(labels) {
17
+ var _labels$notApplicable, _labels$up, _labels$down;
18
+ return {
19
+ notApplicable: (labels === null || labels === void 0 || (_labels$notApplicable = labels.notApplicable) === null || _labels$notApplicable === void 0 ? void 0 : _labels$notApplicable.trim()) || _x('Not applicable', 'a statistic with no number to show', 'newspack-plugin'),
20
+ up: (labels === null || labels === void 0 || (_labels$up = labels.up) === null || _labels$up === void 0 ? void 0 : _labels$up.trim()) || _x('Up', 'a statistic that has increased', 'newspack-plugin'),
21
+ down: (labels === null || labels === void 0 || (_labels$down = labels.down) === null || _labels$down === void 0 ? void 0 : _labels$down.trim()) || _x('Down', 'a statistic that has decreased', 'newspack-plugin')
22
+ };
23
+ };
24
+ export var StatCardContext = createContext(null);
25
+ export var useStatCardContext = function useStatCardContext() {
26
+ var context = useContext(StatCardContext);
27
+ var isOrphan = !context;
28
+ useEffect(function () {
29
+ if (!isOrphan) {
30
+ return;
31
+ }
32
+ // eslint-disable-next-line no-console
33
+ console.warn(ORPHAN_MESSAGE);
34
+ }, [isOrphan]);
35
+ if (!context) {
36
+ // A loose slot sizes its figure against the wrong container, which is
37
+ // cosmetic. Nothing above these cards catches an error, so throwing in
38
+ // production would blank an admin screen over it.
39
+ if ('production' !== process.env.NODE_ENV) {
40
+ throw new Error(ORPHAN_MESSAGE);
41
+ }
42
+ return {
43
+ heading: 3,
44
+ labels: resolveStatCardLabels()
45
+ };
46
+ }
47
+ return context;
48
+ };
@@ -0,0 +1,86 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
3
+ var _excluded = ["direction", "tone", "directionLabel", "label", "className", "children"];
4
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
5
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
6
+ /**
7
+ * WordPress dependencies.
8
+ */
9
+ import { forwardRef, useEffect } from '@wordpress/element';
10
+ import { VisuallyHidden } from '@wordpress/ui';
11
+
12
+ /**
13
+ * External dependencies.
14
+ */
15
+ import classnames from 'classnames';
16
+
17
+ /**
18
+ * Internal dependencies.
19
+ */
20
+ import { useStatCardContext } from './context';
21
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
22
+ var glyphs = {
23
+ up: '↑',
24
+ down: '↓'
25
+ };
26
+ var tones = ['positive', 'negative', 'neutral'];
27
+ var Delta = forwardRef(function Delta(_ref, ref) {
28
+ var direction = _ref.direction,
29
+ _ref$tone = _ref.tone,
30
+ tone = _ref$tone === void 0 ? 'neutral' : _ref$tone,
31
+ directionLabel = _ref.directionLabel,
32
+ label = _ref.label,
33
+ className = _ref.className,
34
+ children = _ref.children,
35
+ props = _objectWithoutProperties(_ref, _excluded);
36
+ var _useStatCardContext = useStatCardContext(),
37
+ labels = _useStatCardContext.labels;
38
+ useEffect(function () {
39
+ if ('production' === process.env.NODE_ENV) {
40
+ return;
41
+ }
42
+ if (!glyphs[direction]) {
43
+ // eslint-disable-next-line no-console
44
+ console.warn("StatCard.Delta: unknown direction \"".concat(direction, "\". Use one of ").concat(Object.keys(glyphs).join(', '), "."));
45
+ }
46
+ if (!tones.includes(tone)) {
47
+ // eslint-disable-next-line no-console
48
+ console.warn("StatCard.Delta: unknown tone \"".concat(tone, "\", falling back to neutral. Use one of ").concat(tones.join(', '), "."));
49
+ }
50
+ }, [direction, tone]);
51
+ var directions = {
52
+ up: labels.up,
53
+ down: labels.down
54
+ };
55
+ var glyph = glyphs[direction];
56
+ // Trimmed, and `||` not `??`: a blank label is a missing one. The fallback
57
+ // is keyed on the arrow's own direction, so an unrecognised one goes
58
+ // unspoken rather than announced as its opposite; words the caller wrote
59
+ // are honoured either way.
60
+ var named = label === null || label === void 0 ? void 0 : label.trim();
61
+ var spoken = (directionLabel === null || directionLabel === void 0 ? void 0 : directionLabel.trim()) || directions[direction];
62
+ var classes = classnames('newspack-stat-card__delta', 'neutral' !== tone && tones.includes(tone) && "newspack-stat-card__delta--".concat(tone), className);
63
+ return /*#__PURE__*/_jsx("span", _objectSpread(_objectSpread({
64
+ ref: ref,
65
+ className: classes
66
+ }, props), {}, {
67
+ children: named ? /*#__PURE__*/_jsxs(_Fragment, {
68
+ children: [/*#__PURE__*/_jsxs("span", {
69
+ "aria-hidden": "true",
70
+ children: [glyph, children]
71
+ }), /*#__PURE__*/_jsx(VisuallyHidden, {
72
+ render: /*#__PURE__*/_jsx("span", {}),
73
+ children: named
74
+ })]
75
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
76
+ children: [glyph && /*#__PURE__*/_jsx("span", {
77
+ "aria-hidden": "true",
78
+ children: glyph
79
+ }), spoken && /*#__PURE__*/_jsx(VisuallyHidden, {
80
+ render: /*#__PURE__*/_jsx("span", {}),
81
+ children: "".concat(spoken, " ")
82
+ }), children]
83
+ })
84
+ }));
85
+ });
86
+ export default Delta;