@box/blueprint-web 18.8.0 → 18.9.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.
@@ -1,3 +1,4 @@
1
+ import { type AnimationEventHandler } from 'react';
1
2
  import { type BadgeProps } from './types';
2
3
  export type BaseBadgeProps = Omit<BadgeProps, 'children'> & {
3
4
  /** The color of the badge background. */
@@ -5,5 +6,9 @@ export type BaseBadgeProps = Omit<BadgeProps, 'children'> & {
5
6
  className?: string;
6
7
  /** The child element to display in the badge: string for NumericBadge and svg for IconBadge). */
7
8
  children?: React.ReactElement;
9
+ 'data-bp-animated'?: 'true' | 'false';
10
+ 'data-bp-animation-generation'?: number;
11
+ 'data-state'?: 'idle' | 'playing';
12
+ onAnimationEnd?: AnimationEventHandler<HTMLDivElement>;
8
13
  };
9
- export declare const BaseBadge: ({ backgroundColor, className, children, size, offset }: BaseBadgeProps) => import("react/jsx-runtime").JSX.Element;
14
+ export declare const BaseBadge: ({ backgroundColor, className, children, size, offset, "data-bp-animated": dataBpAnimated, "data-bp-animation-generation": dataBpAnimationGeneration, "data-state": dataState, onAnimationEnd, }: BaseBadgeProps) => import("react/jsx-runtime").JSX.Element;
@@ -8,11 +8,19 @@ const BaseBadge = ({
8
8
  className,
9
9
  children,
10
10
  size,
11
- offset
11
+ offset,
12
+ 'data-bp-animated': dataBpAnimated,
13
+ 'data-bp-animation-generation': dataBpAnimationGeneration,
14
+ 'data-state': dataState,
15
+ onAnimationEnd
12
16
  }) => {
13
17
  const offsetStyle = mapValues(offset, value => Number.isInteger(value) ? `${value}px` : `0`);
14
18
  return jsx("div", {
15
19
  className: clsx(className, styles.badge, styles[`${size}SizeBadge`]),
20
+ "data-bp-animated": dataBpAnimated,
21
+ "data-bp-animation-generation": dataBpAnimationGeneration,
22
+ "data-state": dataState,
23
+ onAnimationEnd: onAnimationEnd,
16
24
  style: {
17
25
  backgroundColor,
18
26
  ...offsetStyle
@@ -1,4 +1,4 @@
1
1
  import '../index.css';
2
- var styles = {"badgeContainer":"bp_base_badge_module_badgeContainer--cc070","badge":"bp_base_badge_module_badge--cc070","smallSizeBadge":"bp_base_badge_module_smallSizeBadge--cc070","mediumSizeBadge":"bp_base_badge_module_mediumSizeBadge--cc070","largeSizeBadge":"bp_base_badge_module_largeSizeBadge--cc070","xlargeSizeBadge":"bp_base_badge_module_xlargeSizeBadge--cc070","xxlargeSizeBadge":"bp_base_badge_module_xxlargeSizeBadge--cc070","iconBadge":"bp_base_badge_module_iconBadge--cc070","interactiveWrapper":"bp_base_badge_module_interactiveWrapper--cc070","statusBadge":"bp_base_badge_module_statusBadge--cc070","numericBadge":"bp_base_badge_module_numericBadge--cc070","numericBadgeSingleDigit":"bp_base_badge_module_numericBadgeSingleDigit--cc070","numericBadgeMoreDigits":"bp_base_badge_module_numericBadgeMoreDigits--cc070"};
2
+ var styles = {"badgeContainer":"bp_base_badge_module_badgeContainer--44c64","badge":"bp_base_badge_module_badge--44c64","smallSizeBadge":"bp_base_badge_module_smallSizeBadge--44c64","mediumSizeBadge":"bp_base_badge_module_mediumSizeBadge--44c64","largeSizeBadge":"bp_base_badge_module_largeSizeBadge--44c64","xlargeSizeBadge":"bp_base_badge_module_xlargeSizeBadge--44c64","xxlargeSizeBadge":"bp_base_badge_module_xxlargeSizeBadge--44c64","iconBadge":"bp_base_badge_module_iconBadge--44c64","interactiveWrapper":"bp_base_badge_module_interactiveWrapper--44c64","statusBadge":"bp_base_badge_module_statusBadge--44c64","numericBadge":"bp_base_badge_module_numericBadge--44c64","bpNumericBadgePopIn":"bp_base_badge_module_bpNumericBadgePopIn--44c64","numericBadgeSingleDigit":"bp_base_badge_module_numericBadgeSingleDigit--44c64","numericBadgeMoreDigits":"bp_base_badge_module_numericBadgeMoreDigits--44c64"};
3
3
 
4
4
  export { styles as default };
@@ -1,8 +1,12 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { WatermelonRed110 } from '@box/blueprint-web-assets/tokens/tokens';
3
3
  import clsx from 'clsx';
4
- import { forwardRef, Children, cloneElement } from 'react';
4
+ import { forwardRef, useState, useCallback, Children, cloneElement } from 'react';
5
+ import '../blueprint-configuration-context/blueprint-configuration-context.js';
6
+ import '../blueprint-configuration-context/consts.js';
7
+ import { useBlueprintConfiguration } from '../blueprint-configuration-context/useBlueprintConfiguration.js';
5
8
  import { Text } from '../text/text.js';
9
+ import { useOneShotAnimation, ONE_SHOT_ANIMATION_GENERATION_ATTRIBUTE } from '../utils/use-one-shot-animation.js';
6
10
  import { useUniqueId } from '../utils/useUniqueId.js';
7
11
  import { BaseBadge } from './base-badge.js';
8
12
  import styles from './base-badge.module.js';
@@ -14,6 +18,32 @@ const NumericBadge = /*#__PURE__*/forwardRef((props, forwardedRef) => {
14
18
  offset,
15
19
  render
16
20
  } = props;
21
+ const {
22
+ componentsWithAnimationEnabled
23
+ } = useBlueprintConfiguration();
24
+ const isAnimationEnabled = componentsWithAnimationEnabled.includes('NumericBadge');
25
+ const {
26
+ animationState,
27
+ animationGeneration,
28
+ onAnimationEnd
29
+ } = useOneShotAnimation(value, isAnimationEnabled);
30
+ // First paint must play enter: the hook's initial trigger is idle and does not
31
+ // auto-play on mount. Flip to entered on animationend so idle does not loop.
32
+ const [hasEntered, setHasEntered] = useState(false);
33
+ const handleAnimationEnd = useCallback(event => {
34
+ if (event.target !== event.currentTarget) {
35
+ return;
36
+ }
37
+ // A completion from the previous keyed node must not finish this generation.
38
+ if (event.currentTarget.getAttribute(ONE_SHOT_ANIMATION_GENERATION_ATTRIBUTE) !== String(animationGeneration)) {
39
+ return;
40
+ }
41
+ if (!hasEntered) {
42
+ setHasEntered(true);
43
+ }
44
+ onAnimationEnd(event);
45
+ }, [animationGeneration, hasEntered, onAnimationEnd]);
46
+ const animationDataState = isAnimationEnabled && (!hasEntered || animationState === 'playing') ? 'playing' : 'idle';
17
47
  const valueId = useUniqueId('numeric-badge-value');
18
48
  // In the case of a deeply nested child (e.g., the child is wrapped by a tooltip), the render prop must be passed instead of children to correctly apply aria-describedby.
19
49
  let badgeChild;
@@ -36,7 +66,11 @@ const NumericBadge = /*#__PURE__*/forwardRef((props, forwardedRef) => {
36
66
  [styles.numericBadgeMoreDigits]: value.length > 1,
37
67
  [styles.numericBadgeSingleDigit]: value.length === 1
38
68
  }),
69
+ "data-bp-animated": isAnimationEnabled ? 'true' : 'false',
70
+ "data-bp-animation-generation": animationGeneration,
71
+ "data-state": animationDataState,
39
72
  offset: offset,
73
+ onAnimationEnd: handleAnimationEnd,
40
74
  size: "medium",
41
75
  children: jsx(Text, {
42
76
  as: "span",
@@ -45,7 +79,7 @@ const NumericBadge = /*#__PURE__*/forwardRef((props, forwardedRef) => {
45
79
  variant: "labelBold",
46
80
  children: value
47
81
  })
48
- })]
82
+ }, animationGeneration)]
49
83
  });
50
84
  });
51
85
 
@@ -2542,12 +2542,12 @@
2542
2542
  transition:none;
2543
2543
  }
2544
2544
  }
2545
- .bp_base_badge_module_badgeContainer--cc070{
2545
+ .bp_base_badge_module_badgeContainer--44c64{
2546
2546
  display:inline-flex;
2547
2547
  position:relative;
2548
2548
  }
2549
2549
 
2550
- .bp_base_badge_module_badge--cc070{
2550
+ .bp_base_badge_module_badge--44c64{
2551
2551
  align-items:center;
2552
2552
  border:var(--border-2) solid var(--border-badge-border);
2553
2553
  border-radius:var(--radius-half);
@@ -2559,37 +2559,37 @@
2559
2559
  user-select:none;
2560
2560
  }
2561
2561
 
2562
- .bp_base_badge_module_smallSizeBadge--cc070{
2562
+ .bp_base_badge_module_smallSizeBadge--44c64{
2563
2563
  height:var(--size-2);
2564
2564
  min-width:var(--size-2);
2565
2565
  }
2566
2566
 
2567
- .bp_base_badge_module_mediumSizeBadge--cc070{
2567
+ .bp_base_badge_module_mediumSizeBadge--44c64{
2568
2568
  height:var(--size-3);
2569
2569
  min-width:var(--size-3);
2570
2570
  }
2571
2571
 
2572
- .bp_base_badge_module_largeSizeBadge--cc070{
2572
+ .bp_base_badge_module_largeSizeBadge--44c64{
2573
2573
  height:var(--size-4);
2574
2574
  min-width:var(--size-4);
2575
2575
  }
2576
2576
 
2577
- .bp_base_badge_module_xlargeSizeBadge--cc070{
2577
+ .bp_base_badge_module_xlargeSizeBadge--44c64{
2578
2578
  height:var(--size-5);
2579
2579
  min-width:var(--size-5);
2580
2580
  }
2581
2581
 
2582
- .bp_base_badge_module_xxlargeSizeBadge--cc070{
2582
+ .bp_base_badge_module_xxlargeSizeBadge--44c64{
2583
2583
  height:var(--size-6);
2584
2584
  min-width:var(--size-6);
2585
2585
  }
2586
2586
 
2587
- .bp_base_badge_module_iconBadge--cc070{
2587
+ .bp_base_badge_module_iconBadge--44c64{
2588
2588
  bottom:-5%;
2589
2589
  right:-5%;
2590
2590
  }
2591
2591
 
2592
- .bp_base_badge_module_interactiveWrapper--cc070{
2592
+ .bp_base_badge_module_interactiveWrapper--44c64{
2593
2593
  border:var(--border-2) solid #0000;
2594
2594
  border-radius:50%;
2595
2595
  bottom:calc(-5% - var(--border-2)/2);
@@ -2597,22 +2597,22 @@
2597
2597
  position:absolute;
2598
2598
  right:calc(-5% - var(--border-2)/2);
2599
2599
  }
2600
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_smallSizeBadge--cc070{
2600
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_smallSizeBadge--44c64{
2601
2601
  width:var(--size-2);
2602
2602
  }
2603
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_mediumSizeBadge--cc070{
2603
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_mediumSizeBadge--44c64{
2604
2604
  width:var(--size-3);
2605
2605
  }
2606
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_largeSizeBadge--cc070{
2606
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_largeSizeBadge--44c64{
2607
2607
  width:var(--size-4);
2608
2608
  }
2609
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_xlargeSizeBadge--cc070{
2609
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_xlargeSizeBadge--44c64{
2610
2610
  width:var(--size-5);
2611
2611
  }
2612
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_xxlargeSizeBadge--cc070{
2612
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_xxlargeSizeBadge--44c64{
2613
2613
  width:var(--size-6);
2614
2614
  }
2615
- .bp_base_badge_module_interactiveWrapper--cc070::before{
2615
+ .bp_base_badge_module_interactiveWrapper--44c64::before{
2616
2616
  content:"";
2617
2617
  left:calc(var(--size-6)*-1/4);
2618
2618
  min-height:var(--size-6);
@@ -2621,36 +2621,60 @@
2621
2621
  top:calc(var(--size-6)*-1/4);
2622
2622
  }
2623
2623
 
2624
- .bp_base_badge_module_statusBadge--cc070{
2624
+ .bp_base_badge_module_statusBadge--44c64{
2625
2625
  bottom:-20%;
2626
2626
  right:-20%;
2627
2627
  }
2628
2628
 
2629
- .bp_base_badge_module_statusBadge--cc070.bp_base_badge_module_largeSizeBadge--cc070{
2629
+ .bp_base_badge_module_statusBadge--44c64.bp_base_badge_module_largeSizeBadge--44c64{
2630
2630
  bottom:-22%;
2631
2631
  right:-22%;
2632
2632
  }
2633
2633
 
2634
- .bp_base_badge_module_statusBadge--cc070.bp_base_badge_module_xlargeSizeBadge--cc070{
2634
+ .bp_base_badge_module_statusBadge--44c64.bp_base_badge_module_xlargeSizeBadge--44c64{
2635
2635
  bottom:-15%;
2636
2636
  right:-15%;
2637
2637
  }
2638
2638
 
2639
- .bp_base_badge_module_statusBadge--cc070.bp_base_badge_module_xxlargeSizeBadge--cc070{
2639
+ .bp_base_badge_module_statusBadge--44c64.bp_base_badge_module_xxlargeSizeBadge--44c64{
2640
2640
  bottom:-4%;
2641
2641
  right:-4%;
2642
2642
  }
2643
2643
 
2644
- .bp_base_badge_module_numericBadge--cc070{
2644
+ .bp_base_badge_module_numericBadge--44c64{
2645
+ --bp-badge-enter-x:calc(var(--bp-space-010)*-1);
2646
+ --bp-badge-enter-y:var(--bp-space-010);
2645
2647
  left:60%;
2646
2648
  top:0;
2647
2649
  }
2648
2650
 
2649
- .bp_base_badge_module_numericBadgeSingleDigit--cc070{
2651
+ .bp_base_badge_module_numericBadge--44c64[data-bp-animated=true][data-state=playing]{
2652
+ animation-duration:var(--bp-duration-medium);
2653
+ animation-fill-mode:both;
2654
+ animation-name:bp_base_badge_module_bpNumericBadgePopIn--44c64;
2655
+ animation-timing-function:var(--bp-curve-large-on);
2656
+ }
2657
+ @media (prefers-reduced-motion: reduce){
2658
+ .bp_base_badge_module_numericBadge--44c64[data-bp-animated=true][data-state=playing]{
2659
+ animation:none;
2660
+ }
2661
+ }
2662
+
2663
+ @keyframes bp_base_badge_module_bpNumericBadgePopIn--44c64{
2664
+ from{
2665
+ opacity:var(--bp-opacity-hidden);
2666
+ transform:translate(var(--bp-badge-enter-x), var(--bp-badge-enter-y)) scale(.5);
2667
+ }
2668
+ to{
2669
+ opacity:var(--bp-opacity-visible);
2670
+ transform:none;
2671
+ }
2672
+ }
2673
+ .bp_base_badge_module_numericBadgeSingleDigit--44c64{
2650
2674
  padding:1px;
2651
2675
  }
2652
2676
 
2653
- .bp_base_badge_module_numericBadgeMoreDigits--cc070{
2677
+ .bp_base_badge_module_numericBadgeMoreDigits--44c64{
2654
2678
  padding:1px 3px;
2655
2679
  }
2656
2680
  .bp_icon_button_module_iconButton--17523[data-bp-animated=true]{
@@ -2542,12 +2542,12 @@
2542
2542
  transition:none;
2543
2543
  }
2544
2544
  }
2545
- .bp_base_badge_module_badgeContainer--cc070{
2545
+ .bp_base_badge_module_badgeContainer--44c64{
2546
2546
  display:inline-flex;
2547
2547
  position:relative;
2548
2548
  }
2549
2549
 
2550
- .bp_base_badge_module_badge--cc070{
2550
+ .bp_base_badge_module_badge--44c64{
2551
2551
  align-items:center;
2552
2552
  border:var(--border-2) solid var(--border-badge-border);
2553
2553
  border-radius:var(--radius-half);
@@ -2559,37 +2559,37 @@
2559
2559
  user-select:none;
2560
2560
  }
2561
2561
 
2562
- .bp_base_badge_module_smallSizeBadge--cc070{
2562
+ .bp_base_badge_module_smallSizeBadge--44c64{
2563
2563
  height:var(--size-2);
2564
2564
  min-width:var(--size-2);
2565
2565
  }
2566
2566
 
2567
- .bp_base_badge_module_mediumSizeBadge--cc070{
2567
+ .bp_base_badge_module_mediumSizeBadge--44c64{
2568
2568
  height:var(--size-3);
2569
2569
  min-width:var(--size-3);
2570
2570
  }
2571
2571
 
2572
- .bp_base_badge_module_largeSizeBadge--cc070{
2572
+ .bp_base_badge_module_largeSizeBadge--44c64{
2573
2573
  height:var(--size-4);
2574
2574
  min-width:var(--size-4);
2575
2575
  }
2576
2576
 
2577
- .bp_base_badge_module_xlargeSizeBadge--cc070{
2577
+ .bp_base_badge_module_xlargeSizeBadge--44c64{
2578
2578
  height:var(--size-5);
2579
2579
  min-width:var(--size-5);
2580
2580
  }
2581
2581
 
2582
- .bp_base_badge_module_xxlargeSizeBadge--cc070{
2582
+ .bp_base_badge_module_xxlargeSizeBadge--44c64{
2583
2583
  height:var(--size-6);
2584
2584
  min-width:var(--size-6);
2585
2585
  }
2586
2586
 
2587
- .bp_base_badge_module_iconBadge--cc070{
2587
+ .bp_base_badge_module_iconBadge--44c64{
2588
2588
  bottom:-5%;
2589
2589
  right:-5%;
2590
2590
  }
2591
2591
 
2592
- .bp_base_badge_module_interactiveWrapper--cc070{
2592
+ .bp_base_badge_module_interactiveWrapper--44c64{
2593
2593
  border:var(--border-2) solid #0000;
2594
2594
  border-radius:50%;
2595
2595
  bottom:calc(-5% - var(--border-2)/2);
@@ -2597,22 +2597,22 @@
2597
2597
  position:absolute;
2598
2598
  right:calc(-5% - var(--border-2)/2);
2599
2599
  }
2600
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_smallSizeBadge--cc070{
2600
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_smallSizeBadge--44c64{
2601
2601
  width:var(--size-2);
2602
2602
  }
2603
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_mediumSizeBadge--cc070{
2603
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_mediumSizeBadge--44c64{
2604
2604
  width:var(--size-3);
2605
2605
  }
2606
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_largeSizeBadge--cc070{
2606
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_largeSizeBadge--44c64{
2607
2607
  width:var(--size-4);
2608
2608
  }
2609
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_xlargeSizeBadge--cc070{
2609
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_xlargeSizeBadge--44c64{
2610
2610
  width:var(--size-5);
2611
2611
  }
2612
- .bp_base_badge_module_interactiveWrapper--cc070.bp_base_badge_module_xxlargeSizeBadge--cc070{
2612
+ .bp_base_badge_module_interactiveWrapper--44c64.bp_base_badge_module_xxlargeSizeBadge--44c64{
2613
2613
  width:var(--size-6);
2614
2614
  }
2615
- .bp_base_badge_module_interactiveWrapper--cc070::before{
2615
+ .bp_base_badge_module_interactiveWrapper--44c64::before{
2616
2616
  content:"";
2617
2617
  left:calc(var(--size-6)*-1/4);
2618
2618
  min-height:var(--size-6);
@@ -2621,36 +2621,60 @@
2621
2621
  top:calc(var(--size-6)*-1/4);
2622
2622
  }
2623
2623
 
2624
- .bp_base_badge_module_statusBadge--cc070{
2624
+ .bp_base_badge_module_statusBadge--44c64{
2625
2625
  bottom:-20%;
2626
2626
  right:-20%;
2627
2627
  }
2628
2628
 
2629
- .bp_base_badge_module_statusBadge--cc070.bp_base_badge_module_largeSizeBadge--cc070{
2629
+ .bp_base_badge_module_statusBadge--44c64.bp_base_badge_module_largeSizeBadge--44c64{
2630
2630
  bottom:-22%;
2631
2631
  right:-22%;
2632
2632
  }
2633
2633
 
2634
- .bp_base_badge_module_statusBadge--cc070.bp_base_badge_module_xlargeSizeBadge--cc070{
2634
+ .bp_base_badge_module_statusBadge--44c64.bp_base_badge_module_xlargeSizeBadge--44c64{
2635
2635
  bottom:-15%;
2636
2636
  right:-15%;
2637
2637
  }
2638
2638
 
2639
- .bp_base_badge_module_statusBadge--cc070.bp_base_badge_module_xxlargeSizeBadge--cc070{
2639
+ .bp_base_badge_module_statusBadge--44c64.bp_base_badge_module_xxlargeSizeBadge--44c64{
2640
2640
  bottom:-4%;
2641
2641
  right:-4%;
2642
2642
  }
2643
2643
 
2644
- .bp_base_badge_module_numericBadge--cc070{
2644
+ .bp_base_badge_module_numericBadge--44c64{
2645
+ --bp-badge-enter-x:calc(var(--bp-space-010)*-1);
2646
+ --bp-badge-enter-y:var(--bp-space-010);
2645
2647
  left:60%;
2646
2648
  top:0;
2647
2649
  }
2648
2650
 
2649
- .bp_base_badge_module_numericBadgeSingleDigit--cc070{
2651
+ .bp_base_badge_module_numericBadge--44c64[data-bp-animated=true][data-state=playing]{
2652
+ animation-duration:var(--bp-duration-medium);
2653
+ animation-fill-mode:both;
2654
+ animation-name:bp_base_badge_module_bpNumericBadgePopIn--44c64;
2655
+ animation-timing-function:var(--bp-curve-large-on);
2656
+ }
2657
+ @media (prefers-reduced-motion: reduce){
2658
+ .bp_base_badge_module_numericBadge--44c64[data-bp-animated=true][data-state=playing]{
2659
+ animation:none;
2660
+ }
2661
+ }
2662
+
2663
+ @keyframes bp_base_badge_module_bpNumericBadgePopIn--44c64{
2664
+ from{
2665
+ opacity:var(--bp-opacity-hidden);
2666
+ transform:translate(var(--bp-badge-enter-x), var(--bp-badge-enter-y)) scale(.5);
2667
+ }
2668
+ to{
2669
+ opacity:var(--bp-opacity-visible);
2670
+ transform:none;
2671
+ }
2672
+ }
2673
+ .bp_base_badge_module_numericBadgeSingleDigit--44c64{
2650
2674
  padding:1px;
2651
2675
  }
2652
2676
 
2653
- .bp_base_badge_module_numericBadgeMoreDigits--cc070{
2677
+ .bp_base_badge_module_numericBadgeMoreDigits--44c64{
2654
2678
  padding:1px 3px;
2655
2679
  }
2656
2680
  .bp_icon_button_module_iconButton--17523[data-bp-animated=true]{
@@ -0,0 +1,35 @@
1
+ import { type AnimationEvent } from 'react';
2
+ export type OneShotAnimationState = 'idle' | 'playing';
3
+ /**
4
+ * Stamp this on the animated node together with `key={animationGeneration}`.
5
+ * A completion whose stamp does not match the current generation is ignored.
6
+ */
7
+ export declare const ONE_SHOT_ANIMATION_GENERATION_ATTRIBUTE = "data-bp-animation-generation";
8
+ /**
9
+ * Replays a CSS animation when `trigger` changes by `Object.is`.
10
+ *
11
+ * CSS will not restart while `data-state` stays `'playing'`, so this hook
12
+ * sets `'playing'` on a trigger change and returns to `'idle'` on
13
+ * `animationend`.
14
+ *
15
+ * Each accepted trigger change bumps `animationGeneration`. When remounting is
16
+ * safe, key the animated node with that value and set
17
+ * `data-bp-animation-generation` to the same number so an `animationend` from
18
+ * the previous node cannot idle the new one.
19
+ *
20
+ * If the trigger changes while already playing, the hook goes to `'idle'` first,
21
+ * then `'playing'` again. Use that path when the node must stay mounted (for
22
+ * example a text field that should keep focus and input). The idle update is
23
+ * flushed before paint; a follow-up effect sets `'playing'`.
24
+ *
25
+ * A same-value resubmit (for example the same validation error twice) does not
26
+ * change `trigger` by `Object.is`, so it will not replay. Pass a nonce as
27
+ * `trigger` when you need another play without a value change.
28
+ *
29
+ * A rerender that keeps the same `trigger` after idle does not auto-play.
30
+ */
31
+ export declare function useOneShotAnimation(trigger: unknown, isEnabled: boolean): {
32
+ animationState: OneShotAnimationState;
33
+ animationGeneration: number;
34
+ onAnimationEnd: (event: AnimationEvent) => void;
35
+ };
@@ -0,0 +1,112 @@
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { noop } from './noop.js';
3
+ import { useEnhancedEffect } from './useEnhancedEffect.js';
4
+
5
+ /**
6
+ * Stamp this on the animated node together with `key={animationGeneration}`.
7
+ * A completion whose stamp does not match the current generation is ignored.
8
+ */
9
+ const ONE_SHOT_ANIMATION_GENERATION_ATTRIBUTE = 'data-bp-animation-generation';
10
+ const animationEndNoop = noop;
11
+ const readStampedAnimationGeneration = event => {
12
+ const {
13
+ currentTarget
14
+ } = event;
15
+ if (!(currentTarget instanceof Element)) {
16
+ return null;
17
+ }
18
+ const generation = currentTarget.getAttribute(ONE_SHOT_ANIMATION_GENERATION_ATTRIBUTE);
19
+ if (generation === null) {
20
+ return null;
21
+ }
22
+ return Number(generation);
23
+ };
24
+ /**
25
+ * Replays a CSS animation when `trigger` changes by `Object.is`.
26
+ *
27
+ * CSS will not restart while `data-state` stays `'playing'`, so this hook
28
+ * sets `'playing'` on a trigger change and returns to `'idle'` on
29
+ * `animationend`.
30
+ *
31
+ * Each accepted trigger change bumps `animationGeneration`. When remounting is
32
+ * safe, key the animated node with that value and set
33
+ * `data-bp-animation-generation` to the same number so an `animationend` from
34
+ * the previous node cannot idle the new one.
35
+ *
36
+ * If the trigger changes while already playing, the hook goes to `'idle'` first,
37
+ * then `'playing'` again. Use that path when the node must stay mounted (for
38
+ * example a text field that should keep focus and input). The idle update is
39
+ * flushed before paint; a follow-up effect sets `'playing'`.
40
+ *
41
+ * A same-value resubmit (for example the same validation error twice) does not
42
+ * change `trigger` by `Object.is`, so it will not replay. Pass a nonce as
43
+ * `trigger` when you need another play without a value change.
44
+ *
45
+ * A rerender that keeps the same `trigger` after idle does not auto-play.
46
+ */
47
+ function useOneShotAnimation(trigger, isEnabled) {
48
+ const [animationState, setAnimationState] = useState('idle');
49
+ const [animationGeneration, setAnimationGeneration] = useState(0);
50
+ const previousTriggerRef = useRef(trigger);
51
+ const animationGenerationRef = useRef(0);
52
+ const animationStateRef = useRef(animationState);
53
+ const restartAfterIdleRef = useRef(false);
54
+ animationStateRef.current = animationState;
55
+ const onAnimationEnd = useCallback(event => {
56
+ if (event.target !== event.currentTarget || animationState !== 'playing') {
57
+ return;
58
+ }
59
+ // This handler was created for a generation that a newer trigger replaced.
60
+ if (animationGeneration !== animationGenerationRef.current) {
61
+ return;
62
+ }
63
+ const stampedGeneration = readStampedAnimationGeneration(event);
64
+ // Unstamped callers (no keyed node) keep the previous completion behavior.
65
+ if (stampedGeneration !== null && stampedGeneration !== animationGenerationRef.current) {
66
+ return;
67
+ }
68
+ setAnimationState('idle');
69
+ }, [animationGeneration, animationState]);
70
+ useEnhancedEffect(() => {
71
+ if (!isEnabled) {
72
+ previousTriggerRef.current = trigger;
73
+ restartAfterIdleRef.current = false;
74
+ setAnimationState('idle');
75
+ return;
76
+ }
77
+ if (Object.is(previousTriggerRef.current, trigger)) {
78
+ return;
79
+ }
80
+ previousTriggerRef.current = trigger;
81
+ animationGenerationRef.current += 1;
82
+ setAnimationGeneration(animationGenerationRef.current);
83
+ if (animationStateRef.current === 'playing') {
84
+ restartAfterIdleRef.current = true;
85
+ setAnimationState('idle');
86
+ return;
87
+ }
88
+ restartAfterIdleRef.current = false;
89
+ setAnimationState('playing');
90
+ }, [isEnabled, trigger]);
91
+ useEffect(() => {
92
+ if (!restartAfterIdleRef.current || animationState !== 'idle') {
93
+ return;
94
+ }
95
+ restartAfterIdleRef.current = false;
96
+ setAnimationState('playing');
97
+ }, [animationState]);
98
+ if (!isEnabled) {
99
+ return {
100
+ animationState: 'idle',
101
+ animationGeneration,
102
+ onAnimationEnd: animationEndNoop
103
+ };
104
+ }
105
+ return {
106
+ animationState,
107
+ animationGeneration,
108
+ onAnimationEnd
109
+ };
110
+ }
111
+
112
+ export { ONE_SHOT_ANIMATION_GENERATION_ATTRIBUTE, useOneShotAnimation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@box/blueprint-web",
3
- "version": "18.8.0",
3
+ "version": "18.9.0",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": {
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@ariakit/react": "0.4.21",
57
57
  "@ariakit/react-core": "0.4.21",
58
- "@box/blueprint-web-assets": "^5.13.7",
58
+ "@box/blueprint-web-assets": "^5.13.8",
59
59
  "@internationalized/date": "^3.12.0",
60
60
  "@radix-ui/react-accordion": "1.1.2",
61
61
  "@radix-ui/react-checkbox": "1.0.4",
@@ -85,7 +85,7 @@
85
85
  "devDependencies": {
86
86
  "@box/box-test-client": "^3.2.7",
87
87
  "@box/playwright-utils": "^2.8.5",
88
- "@box/storybook-utils": "^1.4.26",
88
+ "@box/storybook-utils": "^1.4.27",
89
89
  "@figma/code-connect": "1.4.4",
90
90
  "@playwright/test": "1.46.1",
91
91
  "@types/js-yaml": "^4.0.9",