@automattic/jetpack-components 0.70.1 → 0.72.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 (47) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/build/components/dot-pager/index.d.ts +14 -0
  3. package/build/components/dot-pager/index.js +52 -0
  4. package/build/components/number-control/index.js +1 -1
  5. package/build/components/split-button/index.js +2 -2
  6. package/build/components/swipeable/index.d.ts +12 -0
  7. package/build/components/swipeable/index.js +293 -0
  8. package/build/index.d.ts +2 -3
  9. package/build/index.js +2 -3
  10. package/components/dot-pager/README.md +20 -0
  11. package/components/dot-pager/index.tsx +147 -0
  12. package/components/dot-pager/style.scss +80 -0
  13. package/components/number-control/index.jsx +3 -1
  14. package/components/split-button/index.tsx +14 -12
  15. package/components/split-button/style.module.scss +5 -0
  16. package/components/swipeable/README.md +34 -0
  17. package/components/swipeable/index.tsx +425 -0
  18. package/components/swipeable/style.scss +34 -0
  19. package/index.ts +2 -3
  20. package/package.json +1 -3
  21. package/build/components/threat-fixer-button/index.d.ts +0 -17
  22. package/build/components/threat-fixer-button/index.js +0 -56
  23. package/build/components/threat-severity-badge/index.d.ts +0 -4
  24. package/build/components/threat-severity-badge/index.js +0 -13
  25. package/build/components/threats-data-views/constants.d.ts +0 -33
  26. package/build/components/threats-data-views/constants.js +0 -37
  27. package/build/components/threats-data-views/index.d.ts +0 -30
  28. package/build/components/threats-data-views/index.js +0 -413
  29. package/build/components/threats-data-views/threats-status-toggle-group-control.d.ts +0 -16
  30. package/build/components/threats-data-views/threats-status-toggle-group-control.js +0 -95
  31. package/components/threat-fixer-button/index.tsx +0 -101
  32. package/components/threat-fixer-button/styles.module.scss +0 -16
  33. package/components/threat-modal/fixer-state-notice.tsx +0 -63
  34. package/components/threat-modal/index.tsx +0 -109
  35. package/components/threat-modal/styles.module.scss +0 -81
  36. package/components/threat-modal/threat-actions.tsx +0 -93
  37. package/components/threat-modal/threat-fix-confirmation.tsx +0 -54
  38. package/components/threat-modal/threat-fix-details.tsx +0 -65
  39. package/components/threat-modal/threat-notice.tsx +0 -84
  40. package/components/threat-modal/threat-summary.tsx +0 -30
  41. package/components/threat-modal/threat-technical-details.tsx +0 -67
  42. package/components/threat-severity-badge/index.tsx +0 -26
  43. package/components/threat-severity-badge/styles.module.scss +0 -27
  44. package/components/threats-data-views/constants.ts +0 -49
  45. package/components/threats-data-views/index.tsx +0 -534
  46. package/components/threats-data-views/styles.module.scss +0 -40
  47. package/components/threats-data-views/threats-status-toggle-group-control.tsx +0 -158
package/CHANGELOG.md CHANGED
@@ -2,9 +2,26 @@
2
2
 
3
3
  ### This is a list detailing changes for the Jetpack RNA Components package releases.
4
4
 
5
+ ## [0.72.0] - 2025-03-31
6
+ ### Added
7
+ - Add dot-page component. [#42625]
8
+
9
+ ### Changed
10
+ - Implement a continuous loop in testimonial slider. [#42781]
11
+
12
+ ### Fixed
13
+ - Components: Prevent deprecation notices by adding `__next40pxDefaultSize` to controls. [#42677]
14
+
15
+ ## [0.71.0] - 2025-03-26
16
+ ### Removed
17
+ - Remove threat components. [#41654]
18
+
19
+ ### Fixed
20
+ - Split Button: Increase compatibility of components, preventing console warnings. [#42627]
21
+
5
22
  ## [0.70.1] - 2025-03-24
6
23
  ### Fixed
7
- - Fixes admin panel header component layout [#42614]
24
+ - Fix admin panel header component layout. [#42614]
8
25
 
9
26
  ## [0.70.0] - 2025-03-24
10
27
  ### Changed
@@ -1326,6 +1343,8 @@
1326
1343
  ### Changed
1327
1344
  - Update node version requirement to 14.16.1
1328
1345
 
1346
+ [0.72.0]: https://github.com/Automattic/jetpack-components/compare/0.71.0...0.72.0
1347
+ [0.71.0]: https://github.com/Automattic/jetpack-components/compare/0.70.1...0.71.0
1329
1348
  [0.70.1]: https://github.com/Automattic/jetpack-components/compare/0.70.0...0.70.1
1330
1349
  [0.70.0]: https://github.com/Automattic/jetpack-components/compare/0.69.1...0.70.0
1331
1350
  [0.69.1]: https://github.com/Automattic/jetpack-components/compare/0.69.0...0.69.1
@@ -0,0 +1,14 @@
1
+ import { ReactNode } from 'react';
2
+ import './style.scss';
3
+ type DotPagerProps = {
4
+ hasDynamicHeight?: boolean;
5
+ children: ReactNode;
6
+ className?: string;
7
+ onPageSelected?: (index: number) => void;
8
+ isClickEnabled?: boolean;
9
+ rotateTime?: number;
10
+ tracksPrefix?: string;
11
+ tracksFn?: (eventName: string, data?: Record<string, unknown>) => void;
12
+ };
13
+ declare const DotPager: ({ hasDynamicHeight, children, className, onPageSelected, isClickEnabled, rotateTime, tracksPrefix, tracksFn, ...props }: DotPagerProps) => import("react/jsx-runtime").JSX.Element;
14
+ export default DotPager;
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from '@wordpress/components';
3
+ import { __, sprintf } from '@wordpress/i18n';
4
+ import clsx from 'clsx';
5
+ import { Children, useState, useEffect, useCallback, useMemo } from 'react';
6
+ import { Swipeable } from '../swipeable/index.js';
7
+ import './style.scss';
8
+ const Controls = ({ currentPage, numberOfPages, setCurrentPage, tracksPrefix, tracksFn, }) => {
9
+ // Create a map of memoized handlers for each page
10
+ const pageHandlers = useMemo(() => Array.from({ length: numberOfPages }, (_, page) => () => {
11
+ tracksFn(tracksPrefix + '_dot_click', {
12
+ current_page: currentPage,
13
+ destination_page: page,
14
+ });
15
+ setCurrentPage(page);
16
+ }), [numberOfPages, currentPage, tracksFn, tracksPrefix, setCurrentPage]);
17
+ if (numberOfPages < 2) {
18
+ return null;
19
+ }
20
+ return (_jsx("ul", { className: "dot-pager__controls", "aria-label": __('Pager controls', 'jetpack-components'), children: Array.from({ length: numberOfPages }, (_, page) => (_jsx("li", { "aria-current": page === currentPage ? 'page' : undefined, children: _jsx(Button, { className: clsx('dot-pager__control-choose-page', {
21
+ 'dot-pager__control-current': page === currentPage,
22
+ }), disabled: page === currentPage, "aria-label": sprintf(
23
+ /* translators: %1$d: current page number, %2$d: total number of pages */
24
+ __('Page %1$d of %2$d', 'jetpack-components'), page + 1, numberOfPages), onClick: pageHandlers[page] }, page.toString()) }, `page-${page}`))) }));
25
+ };
26
+ const DotPager = ({ hasDynamicHeight = false, children, className = '', onPageSelected, isClickEnabled = false, rotateTime = 0, tracksPrefix = '', tracksFn = () => { }, ...props }) => {
27
+ const normalizedChildren = Children.toArray(children).filter(Boolean);
28
+ const [currentPage, setCurrentPage] = useState(0);
29
+ const [isPaused, setIsPaused] = useState(false);
30
+ const numPages = Children.count(normalizedChildren);
31
+ useEffect(() => {
32
+ if (rotateTime > 0 && numPages > 1 && !isPaused) {
33
+ const timerId = setTimeout(() => {
34
+ // Add 1 to numPages to account for the clones
35
+ setCurrentPage((currentPage + 1) % (numPages + 1));
36
+ }, rotateTime * 1000);
37
+ return () => clearTimeout(timerId);
38
+ }
39
+ }, [currentPage, numPages, rotateTime, isPaused]);
40
+ const handleSelectPage = useCallback((index) => {
41
+ setCurrentPage(index);
42
+ onPageSelected?.(index);
43
+ }, [onPageSelected]);
44
+ const handleMouseEnter = useCallback(() => {
45
+ setIsPaused(true);
46
+ }, []);
47
+ const handleMouseLeave = useCallback(() => {
48
+ setIsPaused(false);
49
+ }, []);
50
+ return (_jsxs("div", { className: clsx('dot-pager', className), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props, children: [_jsx(Controls, { currentPage: currentPage, numberOfPages: numPages, setCurrentPage: handleSelectPage, tracksPrefix: tracksPrefix, tracksFn: tracksFn }), _jsx(Swipeable, { hasDynamicHeight: hasDynamicHeight, onPageSelect: handleSelectPage, currentPage: currentPage, pageClassName: "dot-pager__page", containerClassName: "dot-pager__pages", isClickEnabled: isClickEnabled, children: normalizedChildren })] }));
51
+ };
52
+ export default DotPager;
@@ -11,6 +11,6 @@ TextControl, } from '@wordpress/components';
11
11
  */
12
12
  const NumberControl = ExperimentalNumberControl ||
13
13
  function CustomNumberControl(props) {
14
- return _jsx(TextControl, { type: "number", inputMode: "numeric", ...props });
14
+ return (_jsx(TextControl, { type: "number", inputMode: "numeric", ...props, __next40pxDefaultSize: true }));
15
15
  };
16
16
  export default NumberControl;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ButtonGroup, Button, DropdownMenu } from '@wordpress/components';
2
+ import { Button, Flex, DropdownMenu } from '@wordpress/components';
3
3
  import styles from './style.module.scss';
4
4
  const DownIcon = () => (_jsx("svg", { width: "15", height: "9", fill: "none", xmlns: "http://www.w3.org/2000/svg", viewBox: "10 9 4 7", children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "m18.004 10.555-6.005 5.459-6.004-5.459 1.009-1.11 4.995 4.542 4.996-4.542 1.009 1.11Z" }) }));
5
5
  const SplitButton = ({ variant, controls, popoverProps, toggleProps, label, ...buttonProps }) => {
6
- return (_jsxs(ButtonGroup, { className: styles['split-button'], children: [_jsx(Button, { variant: variant, ...buttonProps, className: styles.button }), _jsx(DropdownMenu, { toggleProps: { variant, className: styles.button, ...toggleProps }, popoverProps: { noArrow: false, ...popoverProps }, icon: _jsx(DownIcon, {}), disableOpenOnArrowDown: true, controls: controls, label: label })] }));
6
+ return (_jsx(Flex, { className: styles['split-button'], children: _jsxs("div", { role: "group", className: "components-button-group", children: [_jsx(Button, { variant: variant, ...buttonProps, className: styles.button }), _jsx(DropdownMenu, { toggleProps: { variant, className: styles.button, ...toggleProps }, popoverProps: { noArrow: false, ...popoverProps }, icon: _jsx(DownIcon, {}), disableOpenOnArrowDown: true, controls: controls, label: label })] }) }));
7
7
  };
8
8
  export default SplitButton;
@@ -0,0 +1,12 @@
1
+ import './style.scss';
2
+ export declare const Swipeable: ({ hasDynamicHeight, children, currentPage, onPageSelect, pageClassName, containerClassName, isClickEnabled, ...otherProps }: {
3
+ [x: string]: any;
4
+ hasDynamicHeight?: boolean;
5
+ children: any;
6
+ currentPage?: number;
7
+ onPageSelect: any;
8
+ pageClassName: any;
9
+ containerClassName: any;
10
+ isClickEnabled: any;
11
+ }) => import("react/jsx-runtime").JSX.Element;
12
+ export default Swipeable;
@@ -0,0 +1,293 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { Children, useState, useLayoutEffect, useRef, useCallback, useEffect } from 'react';
4
+ import './style.scss';
5
+ const OFFSET_THRESHOLD_PERCENTAGE = 0.35; // Percentage of width to travel before we trigger the slider to move to the desired slide.
6
+ const VELOCITY_THRESHOLD = 0.2; // Speed of drag above, before we trigger the slider to move to the desired slide.
7
+ const VERTICAL_THRESHOLD_ANGLE = 55;
8
+ const TRANSITION_DURATION = '300ms';
9
+ /**
10
+ * Custom hook to observe and handle resize events on a DOM element.
11
+ *
12
+ * @return {[React.Dispatch<React.SetStateAction<HTMLElement | null>>, ResizeObserverEntry | null]} Tuple containing setter and entry
13
+ */
14
+ function useResizeObserver() {
15
+ const [observerEntry, setObserverEntry] = useState(null);
16
+ const [node, setNode] = useState(null);
17
+ const observer = useRef(null);
18
+ const disconnect = useCallback(() => observer.current?.disconnect(), []);
19
+ const observe = useCallback(() => {
20
+ observer.current = new ResizeObserver(([entry]) => setObserverEntry(entry));
21
+ if (node) {
22
+ observer.current.observe(node);
23
+ }
24
+ }, [node]);
25
+ useLayoutEffect(() => {
26
+ observe();
27
+ return () => disconnect();
28
+ }, [disconnect, observe]);
29
+ return [setNode, observerEntry];
30
+ }
31
+ /**
32
+ * Gets the drag position and timestamp from a mouse, touch, or pointer event.
33
+ *
34
+ * @param {Event} event - The event object from the drag/touch/pointer interaction
35
+ * @return {object} Object containing x, y coordinates and timestamp of the event
36
+ */
37
+ function getDragPositionAndTime(event) {
38
+ const { timeStamp } = event;
39
+ if (Object.prototype.hasOwnProperty.call(event, 'clientX')) {
40
+ return { x: event.clientX, y: event.clientY, timeStamp };
41
+ }
42
+ if (event.targetTouches[0]) {
43
+ return {
44
+ x: event.targetTouches[0].clientX,
45
+ y: event.targetTouches[0].clientY,
46
+ timeStamp,
47
+ };
48
+ }
49
+ const touch = event.changedTouches[0];
50
+ return { x: touch.clientX, y: touch.clientY, timeStamp };
51
+ }
52
+ /**
53
+ * Calculates the total width needed for all pages in the swipeable component.
54
+ *
55
+ * @param {number} pageWidth - The width of a single page
56
+ * @param {number} numPages - The total number of pages
57
+ * @return {number|null} The total width of all pages or null if pageWidth is not available
58
+ */
59
+ function getPagesWidth(pageWidth, numPages) {
60
+ if (!pageWidth) {
61
+ return null;
62
+ }
63
+ return pageWidth * numPages;
64
+ }
65
+ export const Swipeable = ({ hasDynamicHeight = false, children, currentPage = 0, onPageSelect, pageClassName, containerClassName, isClickEnabled, ...otherProps }) => {
66
+ const prevPageRef = useRef(currentPage);
67
+ const [swipeableArea, setSwipeableArea] = useState(null);
68
+ // TODO: Needs to be added RTL support
69
+ const isRtl = false;
70
+ const [resizeObserverRef, entry] = useResizeObserver();
71
+ const [isTransitioning, setIsTransitioning] = useState(false);
72
+ const [pagesStyle, setPagesStyle] = useState({
73
+ transitionDuration: TRANSITION_DURATION,
74
+ });
75
+ const [dragData, setDragData] = useState(null);
76
+ const pagesRef = useRef(null);
77
+ const numPages = Children.count(children);
78
+ const containerWidth = entry?.contentRect?.width;
79
+ useEffect(() => {
80
+ let timeoutId;
81
+ if ((currentPage === 0 && prevPageRef.current === numPages) ||
82
+ (currentPage === numPages - 1 && prevPageRef.current === -1)) {
83
+ // We are in a real slide after being transitioned from a clone
84
+ // we need to set again the transitionDuration to TRANSITION_DURATION
85
+ // But we need to wait a little bit to avoid enabling it before
86
+ // we moved to the real slide
87
+ timeoutId = setTimeout(() => {
88
+ setPagesStyle(prev => ({ ...prev, transitionDuration: TRANSITION_DURATION }));
89
+ }, 500);
90
+ }
91
+ else if (currentPage === numPages || currentPage < 0) {
92
+ // In a clone slide. Start the transition to the real slide
93
+ setIsTransitioning(true);
94
+ }
95
+ prevPageRef.current = currentPage;
96
+ return () => {
97
+ if (timeoutId) {
98
+ clearTimeout(timeoutId);
99
+ }
100
+ };
101
+ }, [currentPage, numPages]);
102
+ const getOffset = useCallback(index => {
103
+ // Adjust offset to account for the cloned element at the beginning
104
+ const adjustedIndex = index + 1;
105
+ const offset = containerWidth * adjustedIndex;
106
+ return isRtl ? offset : -offset;
107
+ }, [isRtl, containerWidth]);
108
+ const updateEnabled = hasDynamicHeight && numPages > 1;
109
+ // Generate a property that denotes the order of the cards, in order to recalculate height whenever the card order changes.
110
+ const childrenOrder = children.reduce((acc, child) => acc + child.key, '');
111
+ useLayoutEffect(() => {
112
+ if (!updateEnabled) {
113
+ // This is a fix for a bug when you have >1 pages and it update the component to just one but the height is still
114
+ // Related to https://github.com/Automattic/dotcom-forge/issues/2033
115
+ if (pagesStyle?.height) {
116
+ setPagesStyle({ ...pagesStyle, height: undefined });
117
+ }
118
+ return;
119
+ }
120
+ const targetHeight = pagesRef.current?.querySelector('.is-current')
121
+ ?.offsetHeight;
122
+ if (targetHeight && pagesStyle?.height !== targetHeight) {
123
+ setPagesStyle({ ...pagesStyle, height: targetHeight });
124
+ }
125
+ }, [pagesRef, currentPage, pagesStyle, updateEnabled, containerWidth, childrenOrder]);
126
+ const resetDragData = useCallback(() => {
127
+ setPagesStyle(prev => ({
128
+ ...prev,
129
+ transitionDuration: TRANSITION_DURATION,
130
+ }));
131
+ setDragData(null);
132
+ }, [setPagesStyle, setDragData]);
133
+ const handleDragStart = useCallback(event => {
134
+ const position = getDragPositionAndTime(event);
135
+ setSwipeableArea(pagesRef.current?.getBoundingClientRect());
136
+ setDragData({ start: position });
137
+ setPagesStyle(prev => ({ ...prev, transitionDuration: `0ms` })); // Set transition Duration to 0 for smooth dragging.
138
+ }, [setPagesStyle, setDragData]);
139
+ const hasSwipedToNextPage = useCallback(delta => (isRtl ? delta > 0 : delta < 0), [isRtl]);
140
+ const hasSwipedToPreviousPage = useCallback(delta => (isRtl ? delta < 0 : delta > 0), [isRtl]);
141
+ const handleTransitionEnd = useCallback(() => {
142
+ if (!isTransitioning) {
143
+ return;
144
+ }
145
+ setIsTransitioning(false);
146
+ // If we're on the clone slides, jump to the corresponding real slide
147
+ // We set the transitionDuration to 0ms to make invisible the
148
+ // change from the clone to the real slide
149
+ if (currentPage >= numPages) {
150
+ setPagesStyle(prev => ({ ...prev, transitionDuration: '0ms' }));
151
+ onPageSelect(0);
152
+ }
153
+ else if (currentPage < 0) {
154
+ setPagesStyle(prev => ({ ...prev, transitionDuration: '0ms' }));
155
+ onPageSelect(numPages - 1);
156
+ }
157
+ }, [currentPage, numPages, onPageSelect, isTransitioning]);
158
+ const handleDragEnd = useCallback(event => {
159
+ if (!dragData) {
160
+ return; // End early if we are not dragging any more.
161
+ }
162
+ let dragPosition = getDragPositionAndTime(event);
163
+ if (dragPosition.x === 0) {
164
+ dragPosition = dragData.last;
165
+ }
166
+ const delta = dragPosition.x - dragData.start.x;
167
+ const absoluteDelta = Math.abs(delta);
168
+ const velocity = absoluteDelta / (dragPosition.timeStamp - dragData.start.timeStamp);
169
+ const verticalAbsoluteDelta = Math.abs(dragPosition.y - dragData.start.y);
170
+ const angle = (Math.atan2(verticalAbsoluteDelta, absoluteDelta) * 180) / Math.PI;
171
+ // Is click or tap?
172
+ if (velocity === 0 && isClickEnabled) {
173
+ onPageSelect((currentPage + 1) % numPages);
174
+ resetDragData();
175
+ return;
176
+ }
177
+ // Is vertical scroll detected?
178
+ if (angle > VERTICAL_THRESHOLD_ANGLE) {
179
+ resetDragData();
180
+ return;
181
+ }
182
+ const hasMetThreshold = absoluteDelta > OFFSET_THRESHOLD_PERCENTAGE * containerWidth ||
183
+ velocity > VELOCITY_THRESHOLD;
184
+ let newIndex = currentPage;
185
+ if (hasMetThreshold) {
186
+ if (hasSwipedToNextPage(delta)) {
187
+ newIndex = currentPage + 1;
188
+ if (newIndex >= numPages) {
189
+ setIsTransitioning(true);
190
+ }
191
+ }
192
+ else if (hasSwipedToPreviousPage(delta)) {
193
+ newIndex = currentPage - 1;
194
+ if (newIndex < 0) {
195
+ setIsTransitioning(true);
196
+ }
197
+ }
198
+ }
199
+ setPagesStyle(prev => ({
200
+ ...prev,
201
+ transform: `translate3d(${getOffset(newIndex)}px, 0px, 0px)`,
202
+ transitionDuration: TRANSITION_DURATION,
203
+ }));
204
+ onPageSelect(newIndex);
205
+ setDragData(null);
206
+ }, [
207
+ currentPage,
208
+ dragData,
209
+ hasSwipedToNextPage,
210
+ hasSwipedToPreviousPage,
211
+ numPages,
212
+ onPageSelect,
213
+ setPagesStyle,
214
+ containerWidth,
215
+ isClickEnabled,
216
+ resetDragData,
217
+ getOffset,
218
+ ]);
219
+ const handleDrag = useCallback(event => {
220
+ if (!dragData) {
221
+ return;
222
+ }
223
+ const dragPosition = getDragPositionAndTime(event);
224
+ const delta = dragPosition.x - dragData.start.x;
225
+ const absoluteDelta = Math.abs(delta);
226
+ const offset = getOffset(currentPage) + delta;
227
+ setDragData({ ...dragData, last: dragPosition });
228
+ // The user needs to swipe horizontally more then 2 px in order for the canvase to be dragging.
229
+ // We do this so that the user can scroll vertically smother.
230
+ if (absoluteDelta < 3) {
231
+ return;
232
+ }
233
+ setPagesStyle(prev => ({
234
+ ...prev,
235
+ transform: `translate3d(${offset}px, 0px, 0px)`,
236
+ transitionDuration: '0ms',
237
+ }));
238
+ if (!swipeableArea) {
239
+ return;
240
+ }
241
+ // Did the user swipe out of the swipeable area?
242
+ if (dragPosition.x < swipeableArea.left ||
243
+ dragPosition.x > swipeableArea.right ||
244
+ dragPosition.y > swipeableArea.bottom ||
245
+ dragPosition.y < swipeableArea.top) {
246
+ handleDragEnd(event);
247
+ }
248
+ }, [dragData, getOffset, currentPage, swipeableArea, handleDragEnd]);
249
+ const getTouchEvents = useCallback(() => {
250
+ if ('onpointerup' in document) {
251
+ return {
252
+ onPointerDown: handleDragStart,
253
+ onPointerMove: handleDrag,
254
+ onPointerUp: handleDragEnd,
255
+ onPointerLeave: handleDragEnd,
256
+ };
257
+ }
258
+ if ('ondragend' in document) {
259
+ return {
260
+ onDragStart: handleDragStart,
261
+ onDrag: handleDrag,
262
+ onDragEnd: handleDragEnd,
263
+ onDragExit: handleDragEnd,
264
+ };
265
+ }
266
+ if ('ontouchend' in document) {
267
+ return {
268
+ onTouchStart: handleDragStart,
269
+ onTouchMove: handleDrag,
270
+ onTouchEnd: handleDragEnd,
271
+ onTouchCancel: handleDragEnd,
272
+ };
273
+ }
274
+ return null;
275
+ }, [handleDragStart, handleDrag, handleDragEnd]);
276
+ const offset = getOffset(currentPage);
277
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ...getTouchEvents(), className: "swipeable__container", ref: pagesRef, ...otherProps, children: _jsxs("div", { className: clsx('swipeable__pages', containerClassName), style: {
278
+ ...pagesStyle,
279
+ width: getPagesWidth(containerWidth, numPages + 2),
280
+ transform: `translate3d(${offset}px, 0px, 0px)`,
281
+ }, onTransitionEnd: handleTransitionEnd, children: [_jsx("div", { style: { width: `${containerWidth}px` }, className: clsx('swipeable__page', pageClassName, {
282
+ 'is-clone': true,
283
+ 'is-prev': currentPage === 0,
284
+ }), children: Children.toArray(children)[numPages - 1] }, `clone-prev-${numPages - 1}`), Children.map(children, (child, index) => (_jsx("div", { style: { width: `${containerWidth}px` }, className: clsx('swipeable__page', pageClassName, {
285
+ 'is-current': index === currentPage,
286
+ 'is-prev': index < currentPage,
287
+ 'is-next': index > currentPage,
288
+ }), "data-testid": `swipeable-page-${index + 1}`, children: child }, `page-${index}`))), _jsx("div", { style: { width: `${containerWidth}px` }, className: clsx('swipeable__page', pageClassName, {
289
+ 'is-clone': true,
290
+ 'is-next': currentPage === numPages - 1,
291
+ }), children: Children.toArray(children)[0] }, `clone-next-0`)] }) }), _jsx("div", { ref: resizeObserverRef, className: "swipeable__resize-observer" })] }));
292
+ };
293
+ export default Swipeable;
package/build/index.d.ts CHANGED
@@ -28,9 +28,6 @@ export { default as CopyToClipboard } from './components/copy-to-clipboard/index
28
28
  export * from './components/icons/index.js';
29
29
  export { default as SplitButton } from './components/split-button/index.js';
30
30
  export { default as ThemeProvider } from './components/theme-provider/index.js';
31
- export { default as ThreatFixerButton } from './components/threat-fixer-button/index.js';
32
- export { default as ThreatSeverityBadge } from './components/threat-severity-badge/index.js';
33
- export { default as ThreatsDataViews } from './components/threats-data-views/index.js';
34
31
  export { default as Text, H2, H3, Title } from './components/text/index.js';
35
32
  export { default as ToggleControl } from './components/toggle-control/index.js';
36
33
  export { default as NumberControl } from './components/number-control/index.js';
@@ -40,6 +37,7 @@ export { default as Button } from './components/button/index.js';
40
37
  export type { ButtonProps } from './components/button/types.js';
41
38
  export { default as LoadingPlaceholder } from './components/loading-placeholder/index.js';
42
39
  export { default as TermsOfService } from './components/terms-of-service/index.js';
40
+ export { default as Badge } from './components/badge/index.js';
43
41
  export { default as Chip } from './components/chip/index.js';
44
42
  export { default as PricingTable, PricingTableColumn, PricingTableHeader, PricingTableItem, } from './components/pricing-table/index.js';
45
43
  export { default as BoostScoreBar } from './components/boost-score-bar/index.js';
@@ -47,6 +45,7 @@ export { default as BoostScoreGraph } from './components/boost-score-graph/index
47
45
  export { default as ProductPrice } from './components/product-price/index.js';
48
46
  export { default as ProductOffer, IconsCard } from './components/product-offer/index.js';
49
47
  export { default as Dialog } from './components/dialog/index.js';
48
+ export { default as DotPager } from './components/dot-pager/index.js';
50
49
  export { default as DonutMeter } from './components/donut-meter/index.js';
51
50
  export { default as RecordMeterBar } from './components/record-meter-bar/index.js';
52
51
  export { default as ContextualUpgradeTrigger } from './components/contextual-upgrade-trigger/index.js';
package/build/index.js CHANGED
@@ -43,9 +43,6 @@ export { default as CopyToClipboard } from './components/copy-to-clipboard/index
43
43
  export * from './components/icons/index.js';
44
44
  export { default as SplitButton } from './components/split-button/index.js';
45
45
  export { default as ThemeProvider } from './components/theme-provider/index.js';
46
- export { default as ThreatFixerButton } from './components/threat-fixer-button/index.js';
47
- export { default as ThreatSeverityBadge } from './components/threat-severity-badge/index.js';
48
- export { default as ThreatsDataViews } from './components/threats-data-views/index.js';
49
46
  export { default as Text, H2, H3, Title } from './components/text/index.js';
50
47
  export { default as ToggleControl } from './components/toggle-control/index.js';
51
48
  export { default as NumberControl } from './components/number-control/index.js';
@@ -54,6 +51,7 @@ export { default as QRCode } from './components/qr-code/index.js';
54
51
  export { default as Button } from './components/button/index.js';
55
52
  export { default as LoadingPlaceholder } from './components/loading-placeholder/index.js';
56
53
  export { default as TermsOfService } from './components/terms-of-service/index.js';
54
+ export { default as Badge } from './components/badge/index.js';
57
55
  export { default as Chip } from './components/chip/index.js';
58
56
  export { default as PricingTable, PricingTableColumn, PricingTableHeader, PricingTableItem, } from './components/pricing-table/index.js';
59
57
  export { default as BoostScoreBar } from './components/boost-score-bar/index.js';
@@ -61,6 +59,7 @@ export { default as BoostScoreGraph } from './components/boost-score-graph/index
61
59
  export { default as ProductPrice } from './components/product-price/index.js';
62
60
  export { default as ProductOffer, IconsCard } from './components/product-offer/index.js';
63
61
  export { default as Dialog } from './components/dialog/index.js';
62
+ export { default as DotPager } from './components/dot-pager/index.js';
64
63
  export { default as DonutMeter } from './components/donut-meter/index.js';
65
64
  export { default as RecordMeterBar } from './components/record-meter-bar/index.js';
66
65
  export { default as ContextualUpgradeTrigger } from './components/contextual-upgrade-trigger/index.js';
@@ -0,0 +1,20 @@
1
+ # Dot pager
2
+
3
+ `DotPager` wraps all its child components into a browsable container that shows only one child at time.
4
+
5
+ ## Usage
6
+
7
+ ```js
8
+ import { DotPager } from '@automattic/jetpack-components';
9
+
10
+
11
+ function myDotPager() {
12
+ return (
13
+ <DotPager>
14
+ <div>Item 1</div>
15
+ <div>Item 2</div>
16
+ <div>Item 3</div>
17
+ </DotPager>
18
+ );
19
+ }
20
+ ```
@@ -0,0 +1,147 @@
1
+ import { Button } from '@wordpress/components';
2
+ import { __, sprintf } from '@wordpress/i18n';
3
+ import clsx from 'clsx';
4
+ import React, { Children, useState, useEffect, ReactNode, useCallback, useMemo } from 'react';
5
+ import { Swipeable } from '../swipeable/index.js';
6
+
7
+ import './style.scss';
8
+
9
+ type ControlsProps = {
10
+ currentPage: number;
11
+ numberOfPages: number;
12
+ setCurrentPage: ( page: number ) => void;
13
+ tracksPrefix: string;
14
+ tracksFn: ( eventName: string, data?: Record< string, unknown > ) => void;
15
+ };
16
+
17
+ const Controls = ( {
18
+ currentPage,
19
+ numberOfPages,
20
+ setCurrentPage,
21
+ tracksPrefix,
22
+ tracksFn,
23
+ }: ControlsProps ) => {
24
+ // Create a map of memoized handlers for each page
25
+ const pageHandlers = useMemo(
26
+ () =>
27
+ Array.from( { length: numberOfPages }, ( _, page ) => () => {
28
+ tracksFn( tracksPrefix + '_dot_click', {
29
+ current_page: currentPage,
30
+ destination_page: page,
31
+ } );
32
+ setCurrentPage( page );
33
+ } ),
34
+ [ numberOfPages, currentPage, tracksFn, tracksPrefix, setCurrentPage ]
35
+ );
36
+
37
+ if ( numberOfPages < 2 ) {
38
+ return null;
39
+ }
40
+
41
+ return (
42
+ <ul className="dot-pager__controls" aria-label={ __( 'Pager controls', 'jetpack-components' ) }>
43
+ { Array.from( { length: numberOfPages }, ( _, page ) => (
44
+ <li key={ `page-${ page }` } aria-current={ page === currentPage ? 'page' : undefined }>
45
+ <Button
46
+ key={ page.toString() }
47
+ className={ clsx( 'dot-pager__control-choose-page', {
48
+ 'dot-pager__control-current': page === currentPage,
49
+ } ) }
50
+ disabled={ page === currentPage }
51
+ aria-label={ sprintf(
52
+ /* translators: %1$d: current page number, %2$d: total number of pages */
53
+ __( 'Page %1$d of %2$d', 'jetpack-components' ),
54
+ page + 1,
55
+ numberOfPages
56
+ ) }
57
+ onClick={ pageHandlers[ page ] }
58
+ />
59
+ </li>
60
+ ) ) }
61
+ </ul>
62
+ );
63
+ };
64
+
65
+ type DotPagerProps = {
66
+ hasDynamicHeight?: boolean;
67
+ children: ReactNode;
68
+ className?: string;
69
+ onPageSelected?: ( index: number ) => void;
70
+ isClickEnabled?: boolean;
71
+ rotateTime?: number;
72
+ tracksPrefix?: string;
73
+ tracksFn?: ( eventName: string, data?: Record< string, unknown > ) => void;
74
+ };
75
+
76
+ const DotPager = ( {
77
+ hasDynamicHeight = false,
78
+ children,
79
+ className = '',
80
+ onPageSelected,
81
+ isClickEnabled = false,
82
+ rotateTime = 0,
83
+ tracksPrefix = '',
84
+ tracksFn = () => {},
85
+ ...props
86
+ }: DotPagerProps ) => {
87
+ const normalizedChildren = Children.toArray( children ).filter( Boolean );
88
+ const [ currentPage, setCurrentPage ] = useState( 0 );
89
+ const [ isPaused, setIsPaused ] = useState( false );
90
+ const numPages = Children.count( normalizedChildren );
91
+
92
+ useEffect( () => {
93
+ if ( rotateTime > 0 && numPages > 1 && ! isPaused ) {
94
+ const timerId = setTimeout( () => {
95
+ // Add 1 to numPages to account for the clones
96
+ setCurrentPage( ( currentPage + 1 ) % ( numPages + 1 ) );
97
+ }, rotateTime * 1000 );
98
+
99
+ return () => clearTimeout( timerId );
100
+ }
101
+ }, [ currentPage, numPages, rotateTime, isPaused ] );
102
+
103
+ const handleSelectPage = useCallback(
104
+ ( index: number ) => {
105
+ setCurrentPage( index );
106
+ onPageSelected?.( index );
107
+ },
108
+ [ onPageSelected ]
109
+ );
110
+
111
+ const handleMouseEnter = useCallback( () => {
112
+ setIsPaused( true );
113
+ }, [] );
114
+
115
+ const handleMouseLeave = useCallback( () => {
116
+ setIsPaused( false );
117
+ }, [] );
118
+
119
+ return (
120
+ <div
121
+ className={ clsx( 'dot-pager', className ) }
122
+ onMouseEnter={ handleMouseEnter }
123
+ onMouseLeave={ handleMouseLeave }
124
+ { ...props }
125
+ >
126
+ <Controls
127
+ currentPage={ currentPage }
128
+ numberOfPages={ numPages }
129
+ setCurrentPage={ handleSelectPage }
130
+ tracksPrefix={ tracksPrefix }
131
+ tracksFn={ tracksFn }
132
+ />
133
+ <Swipeable
134
+ hasDynamicHeight={ hasDynamicHeight }
135
+ onPageSelect={ handleSelectPage }
136
+ currentPage={ currentPage }
137
+ pageClassName="dot-pager__page"
138
+ containerClassName="dot-pager__pages"
139
+ isClickEnabled={ isClickEnabled }
140
+ >
141
+ { normalizedChildren }
142
+ </Swipeable>
143
+ </div>
144
+ );
145
+ };
146
+
147
+ export default DotPager;