@box/blueprint-web 18.7.1 → 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.
- package/dist/lib-esm/avatar/avatar-group/avatar-group.d.ts +2 -0
- package/dist/lib-esm/avatar/avatar-group/avatar-group.js +156 -0
- package/dist/lib-esm/avatar/avatar-group/avatar-group.module.js +4 -0
- package/dist/lib-esm/avatar/avatar-group/types.d.ts +27 -0
- package/dist/lib-esm/avatar/avatar-group/use-avatar-group-hover.d.ts +6 -0
- package/dist/lib-esm/avatar/avatar-group/use-avatar-group-hover.js +61 -0
- package/dist/lib-esm/avatar/avatar-group/utils.d.ts +5 -0
- package/dist/lib-esm/avatar/avatar-group/utils.js +31 -0
- package/dist/lib-esm/avatar/avatar.module.js +1 -1
- package/dist/lib-esm/avatar/index.d.ts +2 -0
- package/dist/lib-esm/badge/base-badge.d.ts +6 -1
- package/dist/lib-esm/badge/base-badge.js +9 -1
- package/dist/lib-esm/badge/base-badge.module.js +1 -1
- package/dist/lib-esm/badge/numeric-badge.js +36 -2
- package/dist/lib-esm/components.css +214 -66
- package/dist/lib-esm/index.css +214 -66
- package/dist/lib-esm/index.js +1 -0
- package/dist/lib-esm/tooltip/travel-tooltip.d.ts +20 -0
- package/dist/lib-esm/tooltip/travel-tooltip.js +100 -0
- package/dist/lib-esm/tooltip/travel-tooltip.module.js +4 -0
- package/dist/lib-esm/utils/use-one-shot-animation.d.ts +35 -0
- package/dist/lib-esm/utils/use-one-shot-animation.js +112 -0
- package/package.json +3 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { bpGray10 } from '@box/blueprint-web-assets/tokens/tokens';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { forwardRef, Children, cloneElement, isValidElement } 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';
|
|
8
|
+
import { Focusable } from '../../focusable/focusable.js';
|
|
9
|
+
import { useTravelTooltip, TravelTooltipBubble } from '../../tooltip/travel-tooltip.js';
|
|
10
|
+
import { composeEventHandlers } from '../../utils/composeEventHandlers.js';
|
|
11
|
+
import { useForkRef } from '../../utils/useForkRef.js';
|
|
12
|
+
import { useUniqueId } from '../../utils/useUniqueId.js';
|
|
13
|
+
import { Avatar } from '../avatar.js';
|
|
14
|
+
import styles from './avatar-group.module.js';
|
|
15
|
+
import { useAvatarGroupHover } from './use-avatar-group-hover.js';
|
|
16
|
+
import { getAvatarGroupVisibility } from './utils.js';
|
|
17
|
+
|
|
18
|
+
const overflowColor = bpGray10;
|
|
19
|
+
function isAvatarElement(child) {
|
|
20
|
+
return /*#__PURE__*/isValidElement(child) && child.type === Avatar;
|
|
21
|
+
}
|
|
22
|
+
const AvatarGroup = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
23
|
+
const {
|
|
24
|
+
children,
|
|
25
|
+
className,
|
|
26
|
+
getTooltipContent,
|
|
27
|
+
max,
|
|
28
|
+
overflowLabel,
|
|
29
|
+
overlap,
|
|
30
|
+
ringColor,
|
|
31
|
+
size,
|
|
32
|
+
style,
|
|
33
|
+
onMouseLeave,
|
|
34
|
+
onPointerLeave,
|
|
35
|
+
onBlur,
|
|
36
|
+
...rest
|
|
37
|
+
} = props;
|
|
38
|
+
const {
|
|
39
|
+
componentsWithAnimationEnabled
|
|
40
|
+
} = useBlueprintConfiguration();
|
|
41
|
+
const isAnimationEnabled = componentsWithAnimationEnabled.includes('AvatarGroup');
|
|
42
|
+
const {
|
|
43
|
+
groupRef: hoverGroupRef,
|
|
44
|
+
onGroupLeave,
|
|
45
|
+
onItemEnter
|
|
46
|
+
} = useAvatarGroupHover(isAnimationEnabled);
|
|
47
|
+
const {
|
|
48
|
+
activeTrigger,
|
|
49
|
+
groupRef: travelGroupRef,
|
|
50
|
+
hideFocusIfLeavingGroup,
|
|
51
|
+
hidePointerIfLeavingGroup,
|
|
52
|
+
label,
|
|
53
|
+
showForFocus,
|
|
54
|
+
showForPointer,
|
|
55
|
+
textRef,
|
|
56
|
+
tooltipRef
|
|
57
|
+
} = useTravelTooltip();
|
|
58
|
+
const ref = useForkRef(useForkRef(hoverGroupRef, travelGroupRef), forwardedRef);
|
|
59
|
+
const tooltipId = useUniqueId('avatar-group-tooltip');
|
|
60
|
+
const avatars = Children.toArray(children).filter(isAvatarElement);
|
|
61
|
+
const {
|
|
62
|
+
visibleCount,
|
|
63
|
+
overflowCount
|
|
64
|
+
} = getAvatarGroupVisibility(avatars.length, max);
|
|
65
|
+
const visibleAvatars = avatars.slice(0, visibleCount);
|
|
66
|
+
const hasTooltips = Boolean(getTooltipContent || overflowLabel);
|
|
67
|
+
const customProperties = {
|
|
68
|
+
...(overlap ? {
|
|
69
|
+
'--avatar-group-overlap': overlap
|
|
70
|
+
} : {}),
|
|
71
|
+
...(ringColor ? {
|
|
72
|
+
'--avatar-group-ring-color': ringColor
|
|
73
|
+
} : {})
|
|
74
|
+
};
|
|
75
|
+
const leaveFromPointer = event => {
|
|
76
|
+
if (hidePointerIfLeavingGroup(event)) {
|
|
77
|
+
onGroupLeave();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const leaveFromFocus = event => {
|
|
81
|
+
if (hideFocusIfLeavingGroup(event)) {
|
|
82
|
+
onGroupLeave();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const activateItem = (index, trigger, tooltip, show) => {
|
|
86
|
+
onItemEnter(index);
|
|
87
|
+
if (tooltip) {
|
|
88
|
+
show(trigger, tooltip);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return jsxs("div", {
|
|
92
|
+
...rest,
|
|
93
|
+
ref: ref,
|
|
94
|
+
className: clsx(styles.avatarGroup, className),
|
|
95
|
+
"data-bp-animated": isAnimationEnabled ? 'true' : 'false',
|
|
96
|
+
onBlur: composeEventHandlers(onBlur, leaveFromFocus),
|
|
97
|
+
onMouseLeave: onMouseLeave,
|
|
98
|
+
onPointerLeave: composeEventHandlers(onPointerLeave, leaveFromPointer),
|
|
99
|
+
role: "group",
|
|
100
|
+
style: overlap || ringColor || style ? {
|
|
101
|
+
...customProperties,
|
|
102
|
+
...style
|
|
103
|
+
} : undefined,
|
|
104
|
+
children: [visibleAvatars.map((child, index) => {
|
|
105
|
+
const tooltip = getTooltipContent?.(child.props.text, index);
|
|
106
|
+
return jsx(Focusable, {
|
|
107
|
+
"aria-describedby": tooltip ? tooltipId : undefined,
|
|
108
|
+
focusable: Boolean(tooltip),
|
|
109
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
110
|
+
onFocusVisible: event => activateItem(index, event.currentTarget, tooltip, showForFocus),
|
|
111
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
112
|
+
onPointerEnter: event => activateItem(index, event.currentTarget, tooltip, showForPointer),
|
|
113
|
+
children: jsx("div", {
|
|
114
|
+
className: styles.item,
|
|
115
|
+
"data-avatar-group-item": "",
|
|
116
|
+
children: jsx("div", {
|
|
117
|
+
className: styles.itemMotion,
|
|
118
|
+
children: size ? /*#__PURE__*/cloneElement(child, {
|
|
119
|
+
size
|
|
120
|
+
}) : child
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
}, child.key ?? index);
|
|
124
|
+
}), overflowCount > 0 && jsx(Focusable, {
|
|
125
|
+
"aria-describedby": overflowLabel ? tooltipId : undefined,
|
|
126
|
+
"aria-label": overflowLabel,
|
|
127
|
+
focusable: Boolean(overflowLabel),
|
|
128
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
129
|
+
onFocusVisible: event => activateItem(visibleAvatars.length, event.currentTarget, overflowLabel, showForFocus),
|
|
130
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
131
|
+
onPointerEnter: event => activateItem(visibleAvatars.length, event.currentTarget, overflowLabel, showForPointer),
|
|
132
|
+
children: jsx("div", {
|
|
133
|
+
className: styles.item,
|
|
134
|
+
"data-avatar-group-item": "",
|
|
135
|
+
children: jsx("div", {
|
|
136
|
+
className: styles.itemMotion,
|
|
137
|
+
children: jsx(Avatar, {
|
|
138
|
+
color: overflowColor,
|
|
139
|
+
size: size,
|
|
140
|
+
text: `+${overflowCount}`
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
}), hasTooltips ? jsx(TravelTooltipBubble, {
|
|
145
|
+
animated: isAnimationEnabled,
|
|
146
|
+
id: tooltipId,
|
|
147
|
+
label: label,
|
|
148
|
+
show: Boolean(activeTrigger),
|
|
149
|
+
textRef: textRef,
|
|
150
|
+
tooltipRef: tooltipRef
|
|
151
|
+
}) : null]
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
AvatarGroup.displayName = 'AvatarGroup';
|
|
155
|
+
|
|
156
|
+
export { AvatarGroup };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
import { type AvatarSize } from '../types';
|
|
3
|
+
export interface AvatarGroupProps extends Omit<ComponentPropsWithoutRef<'div'>, 'role'> {
|
|
4
|
+
/**
|
|
5
|
+
* Avatar children to display. Compose with `<Avatar>` nodes — the group clones
|
|
6
|
+
* them to apply `size` and the overlap/ring treatment.
|
|
7
|
+
*/
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/** Maximum number of avatars shown before an overflow `+N` avatar is rendered. */
|
|
10
|
+
max?: number;
|
|
11
|
+
/** Size passed through to each Avatar, including the overflow avatar. */
|
|
12
|
+
size?: AvatarSize;
|
|
13
|
+
/**
|
|
14
|
+
* Overlap distance between adjacent avatars as a CSS length.
|
|
15
|
+
* @default `--bp-space-020` (8px).
|
|
16
|
+
*/
|
|
17
|
+
overlap?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Ring colour around each avatar as a CSS color.
|
|
20
|
+
* @default `--bp-gray-white`.
|
|
21
|
+
*/
|
|
22
|
+
ringColor?: string;
|
|
23
|
+
/** Accessible label for the overflow avatar. */
|
|
24
|
+
overflowLabel?: string;
|
|
25
|
+
/** Tooltip label for a visible avatar. Omit to skip tooltips. */
|
|
26
|
+
getTooltipContent?(text: string | undefined, index: number): string | undefined;
|
|
27
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function applyAvatarGroupHover(items: HTMLElement[], activeIndex: number | null): void;
|
|
2
|
+
export declare function useAvatarGroupHover(enabled: boolean): {
|
|
3
|
+
groupRef: import("react").RefObject<HTMLDivElement>;
|
|
4
|
+
onGroupLeave: () => void;
|
|
5
|
+
onItemEnter: (index: number) => void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
// Transitions.dev avatar hover. Lift is negative so the active item rises.
|
|
4
|
+
const LIFT = -4;
|
|
5
|
+
const SCALE = 1.05;
|
|
6
|
+
// TODO(ewang): confirm duration — 200/140 has no --bp-duration-* pair (medium=150, long=300)
|
|
7
|
+
const DUR_IN = '200ms';
|
|
8
|
+
const DUR_OUT = '140ms';
|
|
9
|
+
// TODO(ewang): confirm curve — Transitions.dev 0.22,1,0.36,1 vs --bp-curve-large-on
|
|
10
|
+
const EASE_IN = 'cubic-bezier(0.22, 1, 0.36, 1)';
|
|
11
|
+
// Leave is a plain ease-out — no spring overshoot.
|
|
12
|
+
const EASE_OUT = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
|
13
|
+
function applyAvatarGroupHover(items, activeIndex) {
|
|
14
|
+
const isLeaving = activeIndex === null;
|
|
15
|
+
const ease = isLeaving ? EASE_OUT : EASE_IN;
|
|
16
|
+
const duration = isLeaving ? DUR_OUT : DUR_IN;
|
|
17
|
+
items.forEach(item => {
|
|
18
|
+
item.style.transitionDuration = duration;
|
|
19
|
+
item.style.transitionTimingFunction = ease;
|
|
20
|
+
});
|
|
21
|
+
items.forEach((item, index) => {
|
|
22
|
+
if (activeIndex === null) {
|
|
23
|
+
item.style.setProperty('--shift', '0px');
|
|
24
|
+
item.style.setProperty('--scale-active', '1');
|
|
25
|
+
item.style.zIndex = '';
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const isActive = index === activeIndex;
|
|
29
|
+
item.style.setProperty('--shift', isActive ? `${LIFT}px` : '0px');
|
|
30
|
+
item.style.setProperty('--scale-active', isActive ? String(SCALE) : '1');
|
|
31
|
+
item.style.zIndex = isActive ? '3' : '';
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function useAvatarGroupHover(enabled) {
|
|
35
|
+
const groupRef = useRef(null);
|
|
36
|
+
const getItems = useCallback(() => {
|
|
37
|
+
if (!groupRef.current) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return [...groupRef.current.querySelectorAll('[data-avatar-group-item]')];
|
|
41
|
+
}, []);
|
|
42
|
+
const onItemEnter = useCallback(index => {
|
|
43
|
+
if (!enabled) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
applyAvatarGroupHover(getItems(), index);
|
|
47
|
+
}, [enabled, getItems]);
|
|
48
|
+
const onGroupLeave = useCallback(() => {
|
|
49
|
+
if (!enabled) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
applyAvatarGroupHover(getItems(), null);
|
|
53
|
+
}, [enabled, getItems]);
|
|
54
|
+
return {
|
|
55
|
+
groupRef,
|
|
56
|
+
onGroupLeave,
|
|
57
|
+
onItemEnter
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { applyAvatarGroupHover, useAvatarGroupHover };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits a group of avatars into the number shown and the leftover overflow count
|
|
3
|
+
* rendered as a `+N` avatar.
|
|
4
|
+
*
|
|
5
|
+
* `max` is the visible count before overflow. When omitted, every avatar is shown.
|
|
6
|
+
*/
|
|
7
|
+
function getVisibleCountForMax(totalCount, max) {
|
|
8
|
+
if (max === Number.POSITIVE_INFINITY) {
|
|
9
|
+
return totalCount;
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isFinite(max)) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
const normalizedMax = Math.max(0, Math.trunc(max));
|
|
15
|
+
return Math.min(totalCount, normalizedMax);
|
|
16
|
+
}
|
|
17
|
+
function getAvatarGroupVisibility(totalCount, max) {
|
|
18
|
+
if (max === undefined) {
|
|
19
|
+
return {
|
|
20
|
+
visibleCount: totalCount,
|
|
21
|
+
overflowCount: 0
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const visibleCount = getVisibleCountForMax(totalCount, max);
|
|
25
|
+
return {
|
|
26
|
+
visibleCount,
|
|
27
|
+
overflowCount: totalCount - visibleCount
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { getAvatarGroupVisibility };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import '../index.css';
|
|
2
|
-
var styles = {"avatar":"bp_avatar_module_avatar--
|
|
2
|
+
var styles = {"avatar":"bp_avatar_module_avatar--42c50","text":"bp_avatar_module_text--42c50","xsmall":"bp_avatar_module_xsmall--42c50","length-1":"bp_avatar_module_length-1--42c50","length-2":"bp_avatar_module_length-2--42c50","small":"bp_avatar_module_small--42c50","medium":"bp_avatar_module_medium--42c50","large":"bp_avatar_module_large--42c50","length-3":"bp_avatar_module_length-3--42c50","length-4":"bp_avatar_module_length-4--42c50","xlarge":"bp_avatar_module_xlarge--42c50","xxlarge":"bp_avatar_module_xxlarge--42c50","badge":"bp_avatar_module_badge--42c50","image":"bp_avatar_module_image--42c50","loading":"bp_avatar_module_loading--42c50","anonymousAvatar":"bp_avatar_module_anonymousAvatar--42c50","iconContainer":"bp_avatar_module_iconContainer--42c50"};
|
|
3
3
|
|
|
4
4
|
export { styles as default };
|
|
@@ -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--
|
|
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
|
|