@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,100 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useRef, useState, useLayoutEffect } from 'react';
|
|
3
|
+
import styles from './travel-tooltip.module.js';
|
|
4
|
+
|
|
5
|
+
const arm = (trigger, nextLabel) => ({
|
|
6
|
+
element: trigger,
|
|
7
|
+
label: nextLabel ?? trigger.dataset.tooltip ?? ''
|
|
8
|
+
});
|
|
9
|
+
const leftTheGroup = event => {
|
|
10
|
+
const next = event.relatedTarget;
|
|
11
|
+
return !(next instanceof Node && event.currentTarget.contains(next));
|
|
12
|
+
};
|
|
13
|
+
function useTravelTooltip() {
|
|
14
|
+
const groupRef = useRef(null);
|
|
15
|
+
const tooltipRef = useRef(null);
|
|
16
|
+
const textRef = useRef(null);
|
|
17
|
+
const [hovered, setHovered] = useState(null);
|
|
18
|
+
const [focused, setFocused] = useState(null);
|
|
19
|
+
const [leader, setLeader] = useState('pointer');
|
|
20
|
+
// Latest modality leads. The other keeps the bubble open after that one leaves the group.
|
|
21
|
+
const leading = leader === 'pointer' ? hovered : focused;
|
|
22
|
+
const trailing = leader === 'pointer' ? focused : hovered;
|
|
23
|
+
const active = leading ?? trailing;
|
|
24
|
+
const activeTrigger = active?.element ?? null;
|
|
25
|
+
const label = active?.label ?? '';
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
const group = groupRef.current;
|
|
28
|
+
const tooltip = tooltipRef.current;
|
|
29
|
+
const text = textRef.current;
|
|
30
|
+
if (!activeTrigger || !group || !tooltip || !text) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const groupBox = group.getBoundingClientRect();
|
|
34
|
+
const triggerBox = activeTrigger.getBoundingClientRect();
|
|
35
|
+
const computed = getComputedStyle(tooltip);
|
|
36
|
+
const pad = Number.parseFloat(computed.paddingLeft) + Number.parseFloat(computed.paddingRight);
|
|
37
|
+
const width = Math.ceil(text.scrollWidth + pad);
|
|
38
|
+
const x = triggerBox.left - groupBox.left + triggerBox.width / 2 - width / 2;
|
|
39
|
+
tooltip.style.setProperty('--travel-tooltip-x', `${x}px`);
|
|
40
|
+
tooltip.style.width = `${width}px`;
|
|
41
|
+
}, [activeTrigger, label]);
|
|
42
|
+
const showForPointer = (trigger, nextLabel) => {
|
|
43
|
+
setHovered(arm(trigger, nextLabel));
|
|
44
|
+
setLeader('pointer');
|
|
45
|
+
};
|
|
46
|
+
const showForFocus = (trigger, nextLabel) => {
|
|
47
|
+
setFocused(arm(trigger, nextLabel));
|
|
48
|
+
setLeader('focus');
|
|
49
|
+
};
|
|
50
|
+
const hidePointerIfLeavingGroup = event => {
|
|
51
|
+
if (!leftTheGroup(event)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
setHovered(null);
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
const hideFocusIfLeavingGroup = event => {
|
|
58
|
+
if (!leftTheGroup(event)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
setFocused(null);
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
activeTrigger,
|
|
66
|
+
groupRef,
|
|
67
|
+
hideFocusIfLeavingGroup,
|
|
68
|
+
hidePointerIfLeavingGroup,
|
|
69
|
+
label,
|
|
70
|
+
showForFocus,
|
|
71
|
+
showForPointer,
|
|
72
|
+
textRef,
|
|
73
|
+
tooltipRef
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function TravelTooltipBubble({
|
|
77
|
+
animated,
|
|
78
|
+
id,
|
|
79
|
+
label,
|
|
80
|
+
show,
|
|
81
|
+
textRef,
|
|
82
|
+
tooltipRef
|
|
83
|
+
}) {
|
|
84
|
+
return jsx("span", {
|
|
85
|
+
ref: tooltipRef,
|
|
86
|
+
"aria-hidden": show ? 'false' : 'true',
|
|
87
|
+
className: styles.tooltip,
|
|
88
|
+
"data-bp-animated": animated ? 'true' : 'false',
|
|
89
|
+
"data-show": show ? 'true' : 'false',
|
|
90
|
+
id: id,
|
|
91
|
+
role: "tooltip",
|
|
92
|
+
children: jsx("span", {
|
|
93
|
+
ref: textRef,
|
|
94
|
+
className: styles.text,
|
|
95
|
+
children: label
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { TravelTooltipBubble, useTravelTooltip };
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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",
|