@automattic/jetpack-components 0.71.0 → 0.72.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/build/components/action-button/index.d.ts +10 -10
  3. package/build/components/action-button/index.js +1 -1
  4. package/build/components/action-popover/types.d.ts +0 -1
  5. package/build/components/admin-page/types.d.ts +0 -1
  6. package/build/components/admin-section/types.d.ts +0 -1
  7. package/build/components/boost-score-graph/uplot-line-chart.js +2 -2
  8. package/build/components/dot-pager/index.d.ts +14 -0
  9. package/build/components/dot-pager/index.js +52 -0
  10. package/build/components/icon-tooltip/index.js +1 -1
  11. package/build/components/icon-tooltip/types.d.ts +0 -1
  12. package/build/components/layout/types.d.ts +0 -1
  13. package/build/components/number-control/index.js +1 -1
  14. package/build/components/popover/index.d.ts +0 -1
  15. package/build/components/pricing-table/types.d.ts +0 -1
  16. package/build/components/product-offer/types.d.ts +0 -1
  17. package/build/components/product-price/types.d.ts +0 -1
  18. package/build/components/radio-control/index.d.ts +0 -1
  19. package/build/components/spinner/index.d.ts +3 -3
  20. package/build/components/spinner/index.js +1 -1
  21. package/build/components/split-button/types.d.ts +0 -1
  22. package/build/components/stat-card/types.d.ts +0 -1
  23. package/build/components/status/index.d.ts +0 -1
  24. package/build/components/swipeable/index.d.ts +12 -0
  25. package/build/components/swipeable/index.js +293 -0
  26. package/build/components/terms-of-service/index.d.ts +0 -1
  27. package/build/components/theme-provider/types.d.ts +0 -1
  28. package/build/components/toggle-control/index.d.ts +0 -1
  29. package/build/index.d.ts +1 -0
  30. package/build/index.js +1 -0
  31. package/components/action-button/index.jsx +1 -1
  32. package/components/dot-pager/README.md +20 -0
  33. package/components/dot-pager/index.tsx +147 -0
  34. package/components/dot-pager/style.scss +80 -0
  35. package/components/number-control/index.jsx +3 -1
  36. package/components/swipeable/README.md +34 -0
  37. package/components/swipeable/index.tsx +425 -0
  38. package/components/swipeable/style.scss +34 -0
  39. package/index.ts +1 -0
  40. package/package.json +4 -4
@@ -0,0 +1,425 @@
1
+ import clsx from 'clsx';
2
+ import React, { Children, useState, useLayoutEffect, useRef, useCallback, useEffect } from 'react';
3
+
4
+ import './style.scss';
5
+
6
+ interface PageStyle {
7
+ transitionDuration: string;
8
+ height?: number;
9
+ transform?: string;
10
+ }
11
+
12
+ const OFFSET_THRESHOLD_PERCENTAGE = 0.35; // Percentage of width to travel before we trigger the slider to move to the desired slide.
13
+ const VELOCITY_THRESHOLD = 0.2; // Speed of drag above, before we trigger the slider to move to the desired slide.
14
+ const VERTICAL_THRESHOLD_ANGLE = 55;
15
+ const TRANSITION_DURATION = '300ms';
16
+
17
+ /**
18
+ * Custom hook to observe and handle resize events on a DOM element.
19
+ *
20
+ * @return {[React.Dispatch<React.SetStateAction<HTMLElement | null>>, ResizeObserverEntry | null]} Tuple containing setter and entry
21
+ */
22
+ function useResizeObserver(): [
23
+ React.Dispatch< React.SetStateAction< HTMLElement | null > >,
24
+ ResizeObserverEntry | null,
25
+ ] {
26
+ const [ observerEntry, setObserverEntry ] = useState< ResizeObserverEntry | null >( null );
27
+ const [ node, setNode ] = useState< HTMLElement | null >( null );
28
+ const observer = useRef< ResizeObserver | null >( null );
29
+
30
+ const disconnect = useCallback( () => observer.current?.disconnect(), [] );
31
+
32
+ const observe = useCallback( () => {
33
+ observer.current = new ResizeObserver( ( [ entry ] ) => setObserverEntry( entry ) );
34
+ if ( node ) {
35
+ observer.current.observe( node );
36
+ }
37
+ }, [ node ] );
38
+
39
+ useLayoutEffect( () => {
40
+ observe();
41
+ return () => disconnect();
42
+ }, [ disconnect, observe ] );
43
+
44
+ return [ setNode, observerEntry ];
45
+ }
46
+
47
+ /**
48
+ * Gets the drag position and timestamp from a mouse, touch, or pointer event.
49
+ *
50
+ * @param {Event} event - The event object from the drag/touch/pointer interaction
51
+ * @return {object} Object containing x, y coordinates and timestamp of the event
52
+ */
53
+ function getDragPositionAndTime( event ) {
54
+ const { timeStamp } = event;
55
+ if ( Object.prototype.hasOwnProperty.call( event, 'clientX' ) ) {
56
+ return { x: event.clientX, y: event.clientY, timeStamp };
57
+ }
58
+
59
+ if ( event.targetTouches[ 0 ] ) {
60
+ return {
61
+ x: event.targetTouches[ 0 ].clientX,
62
+ y: event.targetTouches[ 0 ].clientY,
63
+ timeStamp,
64
+ };
65
+ }
66
+
67
+ const touch = event.changedTouches[ 0 ];
68
+ return { x: touch.clientX, y: touch.clientY, timeStamp };
69
+ }
70
+
71
+ /**
72
+ * Calculates the total width needed for all pages in the swipeable component.
73
+ *
74
+ * @param {number} pageWidth - The width of a single page
75
+ * @param {number} numPages - The total number of pages
76
+ * @return {number|null} The total width of all pages or null if pageWidth is not available
77
+ */
78
+ function getPagesWidth( pageWidth, numPages ) {
79
+ if ( ! pageWidth ) {
80
+ return null;
81
+ }
82
+ return pageWidth * numPages;
83
+ }
84
+
85
+ export const Swipeable = ( {
86
+ hasDynamicHeight = false,
87
+ children,
88
+ currentPage = 0,
89
+ onPageSelect,
90
+ pageClassName,
91
+ containerClassName,
92
+ isClickEnabled,
93
+ ...otherProps
94
+ } ) => {
95
+ const prevPageRef = useRef( currentPage );
96
+ const [ swipeableArea, setSwipeableArea ] = useState< DOMRect | null >( null );
97
+ // TODO: Needs to be added RTL support
98
+ const isRtl = false;
99
+
100
+ const [ resizeObserverRef, entry ] = useResizeObserver();
101
+ const [ isTransitioning, setIsTransitioning ] = useState( false );
102
+
103
+ const [ pagesStyle, setPagesStyle ] = useState< PageStyle >( {
104
+ transitionDuration: TRANSITION_DURATION,
105
+ } );
106
+
107
+ const [ dragData, setDragData ] = useState( null );
108
+
109
+ const pagesRef = useRef< HTMLDivElement >( null );
110
+ const numPages = Children.count( children );
111
+ const containerWidth = entry?.contentRect?.width;
112
+
113
+ useEffect( () => {
114
+ let timeoutId: ReturnType< typeof setTimeout >;
115
+ if (
116
+ ( currentPage === 0 && prevPageRef.current === numPages ) ||
117
+ ( currentPage === numPages - 1 && prevPageRef.current === -1 )
118
+ ) {
119
+ // We are in a real slide after being transitioned from a clone
120
+ // we need to set again the transitionDuration to TRANSITION_DURATION
121
+ // But we need to wait a little bit to avoid enabling it before
122
+ // we moved to the real slide
123
+ timeoutId = setTimeout( () => {
124
+ setPagesStyle( prev => ( { ...prev, transitionDuration: TRANSITION_DURATION } ) );
125
+ }, 500 );
126
+ } else if ( currentPage === numPages || currentPage < 0 ) {
127
+ // In a clone slide. Start the transition to the real slide
128
+ setIsTransitioning( true );
129
+ }
130
+
131
+ prevPageRef.current = currentPage;
132
+ return () => {
133
+ if ( timeoutId ) {
134
+ clearTimeout( timeoutId );
135
+ }
136
+ };
137
+ }, [ currentPage, numPages ] );
138
+
139
+ const getOffset = useCallback(
140
+ index => {
141
+ // Adjust offset to account for the cloned element at the beginning
142
+ const adjustedIndex = index + 1;
143
+ const offset = containerWidth * adjustedIndex;
144
+ return isRtl ? offset : -offset;
145
+ },
146
+ [ isRtl, containerWidth ]
147
+ );
148
+
149
+ const updateEnabled = hasDynamicHeight && numPages > 1;
150
+
151
+ // Generate a property that denotes the order of the cards, in order to recalculate height whenever the card order changes.
152
+ const childrenOrder = children.reduce( ( acc, child ) => acc + child.key, '' );
153
+
154
+ useLayoutEffect( () => {
155
+ if ( ! updateEnabled ) {
156
+ // 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
157
+ // Related to https://github.com/Automattic/dotcom-forge/issues/2033
158
+ if ( pagesStyle?.height ) {
159
+ setPagesStyle( { ...pagesStyle, height: undefined } );
160
+ }
161
+ return;
162
+ }
163
+ const targetHeight = ( pagesRef.current?.querySelector( '.is-current' ) as HTMLElement )
164
+ ?.offsetHeight;
165
+
166
+ if ( targetHeight && pagesStyle?.height !== targetHeight ) {
167
+ setPagesStyle( { ...pagesStyle, height: targetHeight } );
168
+ }
169
+ }, [ pagesRef, currentPage, pagesStyle, updateEnabled, containerWidth, childrenOrder ] );
170
+
171
+ const resetDragData = useCallback( () => {
172
+ setPagesStyle( prev => ( {
173
+ ...prev,
174
+ transitionDuration: TRANSITION_DURATION,
175
+ } ) );
176
+ setDragData( null );
177
+ }, [ setPagesStyle, setDragData ] );
178
+
179
+ const handleDragStart = useCallback(
180
+ event => {
181
+ const position = getDragPositionAndTime( event );
182
+ setSwipeableArea( pagesRef.current?.getBoundingClientRect() );
183
+ setDragData( { start: position } );
184
+ setPagesStyle( prev => ( { ...prev, transitionDuration: `0ms` } ) ); // Set transition Duration to 0 for smooth dragging.
185
+ },
186
+ [ setPagesStyle, setDragData ]
187
+ );
188
+
189
+ const hasSwipedToNextPage = useCallback( delta => ( isRtl ? delta > 0 : delta < 0 ), [ isRtl ] );
190
+ const hasSwipedToPreviousPage = useCallback(
191
+ delta => ( isRtl ? delta < 0 : delta > 0 ),
192
+ [ isRtl ]
193
+ );
194
+
195
+ const handleTransitionEnd = useCallback( () => {
196
+ if ( ! isTransitioning ) {
197
+ return;
198
+ }
199
+
200
+ setIsTransitioning( false );
201
+
202
+ // If we're on the clone slides, jump to the corresponding real slide
203
+ // We set the transitionDuration to 0ms to make invisible the
204
+ // change from the clone to the real slide
205
+ if ( currentPage >= numPages ) {
206
+ setPagesStyle( prev => ( { ...prev, transitionDuration: '0ms' } ) );
207
+ onPageSelect( 0 );
208
+ } else if ( currentPage < 0 ) {
209
+ setPagesStyle( prev => ( { ...prev, transitionDuration: '0ms' } ) );
210
+ onPageSelect( numPages - 1 );
211
+ }
212
+ }, [ currentPage, numPages, onPageSelect, isTransitioning ] );
213
+
214
+ const handleDragEnd = useCallback(
215
+ event => {
216
+ if ( ! dragData ) {
217
+ return; // End early if we are not dragging any more.
218
+ }
219
+
220
+ let dragPosition = getDragPositionAndTime( event );
221
+
222
+ if ( dragPosition.x === 0 ) {
223
+ dragPosition = dragData.last;
224
+ }
225
+
226
+ const delta = dragPosition.x - dragData.start.x;
227
+ const absoluteDelta = Math.abs( delta );
228
+ const velocity = absoluteDelta / ( dragPosition.timeStamp - dragData.start.timeStamp );
229
+
230
+ const verticalAbsoluteDelta = Math.abs( dragPosition.y - dragData.start.y );
231
+ const angle = ( Math.atan2( verticalAbsoluteDelta, absoluteDelta ) * 180 ) / Math.PI;
232
+
233
+ // Is click or tap?
234
+ if ( velocity === 0 && isClickEnabled ) {
235
+ onPageSelect( ( currentPage + 1 ) % numPages );
236
+ resetDragData();
237
+ return;
238
+ }
239
+
240
+ // Is vertical scroll detected?
241
+ if ( angle > VERTICAL_THRESHOLD_ANGLE ) {
242
+ resetDragData();
243
+ return;
244
+ }
245
+
246
+ const hasMetThreshold =
247
+ absoluteDelta > OFFSET_THRESHOLD_PERCENTAGE * containerWidth ||
248
+ velocity > VELOCITY_THRESHOLD;
249
+
250
+ let newIndex = currentPage;
251
+
252
+ if ( hasMetThreshold ) {
253
+ if ( hasSwipedToNextPage( delta ) ) {
254
+ newIndex = currentPage + 1;
255
+ if ( newIndex >= numPages ) {
256
+ setIsTransitioning( true );
257
+ }
258
+ } else if ( hasSwipedToPreviousPage( delta ) ) {
259
+ newIndex = currentPage - 1;
260
+ if ( newIndex < 0 ) {
261
+ setIsTransitioning( true );
262
+ }
263
+ }
264
+ }
265
+
266
+ setPagesStyle( prev => ( {
267
+ ...prev,
268
+ transform: `translate3d(${ getOffset( newIndex ) }px, 0px, 0px)`,
269
+ transitionDuration: TRANSITION_DURATION,
270
+ } ) );
271
+
272
+ onPageSelect( newIndex );
273
+ setDragData( null );
274
+ },
275
+ [
276
+ currentPage,
277
+ dragData,
278
+ hasSwipedToNextPage,
279
+ hasSwipedToPreviousPage,
280
+ numPages,
281
+ onPageSelect,
282
+ setPagesStyle,
283
+ containerWidth,
284
+ isClickEnabled,
285
+ resetDragData,
286
+ getOffset,
287
+ ]
288
+ );
289
+
290
+ const handleDrag = useCallback(
291
+ event => {
292
+ if ( ! dragData ) {
293
+ return;
294
+ }
295
+
296
+ const dragPosition = getDragPositionAndTime( event );
297
+ const delta = dragPosition.x - dragData.start.x;
298
+ const absoluteDelta = Math.abs( delta );
299
+ const offset = getOffset( currentPage ) + delta;
300
+ setDragData( { ...dragData, last: dragPosition } );
301
+ // The user needs to swipe horizontally more then 2 px in order for the canvase to be dragging.
302
+ // We do this so that the user can scroll vertically smother.
303
+ if ( absoluteDelta < 3 ) {
304
+ return;
305
+ }
306
+
307
+ setPagesStyle( prev => ( {
308
+ ...prev,
309
+ transform: `translate3d(${ offset }px, 0px, 0px)`,
310
+ transitionDuration: '0ms',
311
+ } ) );
312
+
313
+ if ( ! swipeableArea ) {
314
+ return;
315
+ }
316
+ // Did the user swipe out of the swipeable area?
317
+ if (
318
+ dragPosition.x < swipeableArea.left ||
319
+ dragPosition.x > swipeableArea.right ||
320
+ dragPosition.y > swipeableArea.bottom ||
321
+ dragPosition.y < swipeableArea.top
322
+ ) {
323
+ handleDragEnd( event );
324
+ }
325
+ },
326
+ [ dragData, getOffset, currentPage, swipeableArea, handleDragEnd ]
327
+ );
328
+
329
+ const getTouchEvents = useCallback( () => {
330
+ if ( 'onpointerup' in document ) {
331
+ return {
332
+ onPointerDown: handleDragStart,
333
+ onPointerMove: handleDrag,
334
+ onPointerUp: handleDragEnd,
335
+ onPointerLeave: handleDragEnd,
336
+ };
337
+ }
338
+
339
+ if ( 'ondragend' in document ) {
340
+ return {
341
+ onDragStart: handleDragStart,
342
+ onDrag: handleDrag,
343
+ onDragEnd: handleDragEnd,
344
+ onDragExit: handleDragEnd,
345
+ };
346
+ }
347
+
348
+ if ( 'ontouchend' in document ) {
349
+ return {
350
+ onTouchStart: handleDragStart,
351
+ onTouchMove: handleDrag,
352
+ onTouchEnd: handleDragEnd,
353
+ onTouchCancel: handleDragEnd,
354
+ };
355
+ }
356
+
357
+ return null;
358
+ }, [ handleDragStart, handleDrag, handleDragEnd ] );
359
+
360
+ const offset = getOffset( currentPage );
361
+
362
+ return (
363
+ <>
364
+ <div
365
+ { ...getTouchEvents() }
366
+ className="swipeable__container"
367
+ ref={ pagesRef }
368
+ { ...otherProps }
369
+ >
370
+ <div
371
+ className={ clsx( 'swipeable__pages', containerClassName ) }
372
+ style={ {
373
+ ...pagesStyle,
374
+ width: getPagesWidth( containerWidth, numPages + 2 ),
375
+ transform: `translate3d(${ offset }px, 0px, 0px)`,
376
+ } }
377
+ onTransitionEnd={ handleTransitionEnd }
378
+ >
379
+ { /* Clone of the last element */ }
380
+ <div
381
+ style={ { width: `${ containerWidth }px` } }
382
+ className={ clsx( 'swipeable__page', pageClassName, {
383
+ 'is-clone': true,
384
+ 'is-prev': currentPage === 0,
385
+ } ) }
386
+ key={ `clone-prev-${ numPages - 1 }` }
387
+ >
388
+ { Children.toArray( children )[ numPages - 1 ] }
389
+ </div>
390
+
391
+ { /* Original elements */ }
392
+ { Children.map( children, ( child, index ) => (
393
+ <div
394
+ style={ { width: `${ containerWidth }px` } }
395
+ className={ clsx( 'swipeable__page', pageClassName, {
396
+ 'is-current': index === currentPage,
397
+ 'is-prev': index < currentPage,
398
+ 'is-next': index > currentPage,
399
+ } ) }
400
+ key={ `page-${ index }` }
401
+ data-testid={ `swipeable-page-${ index + 1 }` }
402
+ >
403
+ { child }
404
+ </div>
405
+ ) ) }
406
+
407
+ { /* Clone of the first element */ }
408
+ <div
409
+ style={ { width: `${ containerWidth }px` } }
410
+ className={ clsx( 'swipeable__page', pageClassName, {
411
+ 'is-clone': true,
412
+ 'is-next': currentPage === numPages - 1,
413
+ } ) }
414
+ key={ `clone-next-0` }
415
+ >
416
+ { Children.toArray( children )[ 0 ] }
417
+ </div>
418
+ </div>
419
+ </div>
420
+ <div ref={ resizeObserverRef } className="swipeable__resize-observer"></div>
421
+ </>
422
+ );
423
+ };
424
+
425
+ export default Swipeable;
@@ -0,0 +1,34 @@
1
+ @import '@automattic/jetpack-base-styles/gutenberg-base-styles';
2
+
3
+ .swipeable__container {
4
+ width: 100%;
5
+ height: 100%;
6
+ position: relative;
7
+ overflow: hidden;
8
+ list-style: none;
9
+ padding: 0;
10
+ touch-action: pan-y;
11
+ }
12
+
13
+ .swipeable__pages {
14
+ position: relative;
15
+ width: 100%;
16
+ height: 100%;
17
+ display: flex;
18
+ align-items: flex-start;
19
+ transition: all 0.5s ease-in-out;
20
+ transition-property: transform, height;
21
+
22
+ @include reduce-motion( "transition" );
23
+ }
24
+
25
+ .swipeable__page {
26
+ width: 100%;
27
+ flex-shrink: 0;
28
+ margin-left: 0;
29
+
30
+ &.is-clone {
31
+ position: relative;
32
+ pointer-events: none; // Prevent interactions with cloned elements
33
+ }
34
+ }
package/index.ts CHANGED
@@ -66,6 +66,7 @@ export { default as BoostScoreGraph } from './components/boost-score-graph/index
66
66
  export { default as ProductPrice } from './components/product-price/index.js';
67
67
  export { default as ProductOffer, IconsCard } from './components/product-offer/index.js';
68
68
  export { default as Dialog } from './components/dialog/index.js';
69
+ export { default as DotPager } from './components/dot-pager/index.js';
69
70
  export { default as DonutMeter } from './components/donut-meter/index.js';
70
71
  export { default as RecordMeterBar } from './components/record-meter-bar/index.js';
71
72
  export { default as ContextualUpgradeTrigger } from './components/contextual-upgrade-trigger/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/jetpack-components",
3
- "version": "0.71.0",
3
+ "version": "0.72.1",
4
4
  "description": "Jetpack Components Package",
5
5
  "author": "Automattic",
6
6
  "license": "GPL-2.0-or-later",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@automattic/format-currency": "1.0.1",
18
- "@automattic/jetpack-boost-score-api": "^0.1.60",
18
+ "@automattic/jetpack-boost-score-api": "^0.1.61",
19
19
  "@automattic/jetpack-api": "^0.20.0",
20
20
  "@automattic/jetpack-script-data": "^0.3.0",
21
21
  "@babel/runtime": "^7",
@@ -32,7 +32,7 @@
32
32
  "prop-types": "^15.7.2",
33
33
  "qrcode.react": "4.2.0",
34
34
  "react-slider": "2.0.5",
35
- "social-logos": "^3.1.18",
35
+ "social-logos": "^3.1.19",
36
36
  "uplot": "1.6.31",
37
37
  "uplot-react": "1.1.4"
38
38
  },
@@ -58,7 +58,7 @@
58
58
  "require-from-string": "2.0.2",
59
59
  "storybook": "8.6.7",
60
60
  "ts-dedent": "2.2.0",
61
- "typescript": "5.0.4",
61
+ "typescript": "5.8.2",
62
62
  "webpack": "5.94.0",
63
63
  "webpack-cli": "6.0.1"
64
64
  },