@box/blueprint-web 16.12.1 → 16.13.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.
- package/dist/lib-esm/components.css +195 -49
- package/dist/lib-esm/index.css +195 -49
- package/dist/lib-esm/inline-table/inline-table.figma.d.ts +1 -0
- package/dist/lib-esm/select/select-context.d.ts +9 -1
- package/dist/lib-esm/select/select-context.js +17 -3
- package/dist/lib-esm/select/select.js +60 -12
- package/dist/lib-esm/select/select.module.js +1 -1
- package/dist/lib-esm/select/types.d.ts +4 -0
- package/dist/lib-esm/select/utils/use-select-exit-animation.d.ts +20 -0
- package/dist/lib-esm/select/utils/use-select-exit-animation.js +153 -0
- package/package.json +3 -3
|
@@ -5,15 +5,20 @@ import { bpSize030, bpIconIconOnLight } from '@box/blueprint-web-assets/tokens/t
|
|
|
5
5
|
import * as ScrollArea from '@radix-ui/react-scroll-area';
|
|
6
6
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
7
7
|
import clsx from 'clsx';
|
|
8
|
-
import { forwardRef, useCallback, useLayoutEffect } from 'react';
|
|
8
|
+
import { forwardRef, useEffect, useCallback, useLayoutEffect } from 'react';
|
|
9
|
+
import '../blueprint-configuration-context/blueprint-configuration-context.js';
|
|
10
|
+
import '../blueprint-configuration-context/consts.js';
|
|
11
|
+
import { useBlueprintConfiguration } from '../blueprint-configuration-context/useBlueprintConfiguration.js';
|
|
9
12
|
import { useBlueprintModernization } from '../blueprint-modernization-context/useBlueprintModernization.js';
|
|
10
13
|
import { InlineError } from '../primitives/inline-error/inline-error.js';
|
|
11
14
|
import { Text } from '../text/text.js';
|
|
12
15
|
import { useLabelable } from '../util-components/labelable/useLabelable.js';
|
|
16
|
+
import { composeEventHandlers } from '../utils/composeEventHandlers.js';
|
|
13
17
|
import { useControllableState } from '../utils/useControllableState.js';
|
|
14
18
|
import { useUniqueId } from '../utils/useUniqueId.js';
|
|
15
19
|
import { SelectProvider, useSelectContext } from './select-context.js';
|
|
16
20
|
import styles from './select.module.js';
|
|
21
|
+
import { useSelectExitAnimation, handleSelectContentExitAnimationEnd } from './utils/use-select-exit-animation.js';
|
|
17
22
|
|
|
18
23
|
const Root = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
19
24
|
const {
|
|
@@ -57,24 +62,44 @@ const Root = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
57
62
|
const {
|
|
58
63
|
enableModernizedComponents
|
|
59
64
|
} = useBlueprintModernization();
|
|
65
|
+
const {
|
|
66
|
+
componentsWithAnimationEnabled
|
|
67
|
+
} = useBlueprintConfiguration();
|
|
68
|
+
const isAnimationEnabled = componentsWithAnimationEnabled.includes('Select');
|
|
60
69
|
const hasError = !!error && !disabled;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
const [openState, setOpenState] = useControllableState({
|
|
71
|
+
prop: open,
|
|
72
|
+
defaultProp: defaultOpen,
|
|
73
|
+
onChange: isOpen => {
|
|
74
|
+
if (!readonly) {
|
|
75
|
+
onOpenChange?.(isOpen);
|
|
76
|
+
}
|
|
67
77
|
}
|
|
68
|
-
};
|
|
78
|
+
});
|
|
79
|
+
const {
|
|
80
|
+
radixOpen,
|
|
81
|
+
isExiting,
|
|
82
|
+
hasExitStyles,
|
|
83
|
+
handleOpenChange,
|
|
84
|
+
completeExit
|
|
85
|
+
} = useSelectExitAnimation({
|
|
86
|
+
open: Boolean(openState),
|
|
87
|
+
setOpen: setOpenState,
|
|
88
|
+
isAnimationEnabled
|
|
89
|
+
});
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (disabled || readonly) {
|
|
92
|
+
setOpenState(prev => prev ? false : prev);
|
|
93
|
+
}
|
|
94
|
+
}, [disabled, readonly, setOpenState]);
|
|
69
95
|
const selectProps = {
|
|
70
96
|
name,
|
|
71
97
|
disabled,
|
|
72
98
|
required,
|
|
73
99
|
value,
|
|
74
|
-
|
|
75
|
-
open: !readonly && isSelectOpen,
|
|
100
|
+
open: !readonly && !disabled && radixOpen,
|
|
76
101
|
onValueChange,
|
|
77
|
-
onOpenChange:
|
|
102
|
+
onOpenChange: handleOpenChange
|
|
78
103
|
};
|
|
79
104
|
const Label = useLabelable(label, selectId, required);
|
|
80
105
|
const inlineErrorId = useUniqueId('inline-error-');
|
|
@@ -104,11 +129,16 @@ const Root = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
104
129
|
className: clsx(styles.container, {
|
|
105
130
|
[styles.disabled]: disabled
|
|
106
131
|
}, className),
|
|
132
|
+
"data-bp-animated": isAnimationEnabled ? 'true' : 'false',
|
|
107
133
|
"data-modern": enableModernizedComponents ? 'true' : 'false',
|
|
108
134
|
children: [jsx(Label, {
|
|
109
135
|
className: styles.label,
|
|
110
136
|
hideLabel: hideLabel
|
|
111
137
|
}), jsx(SelectProvider, {
|
|
138
|
+
completeExit: completeExit,
|
|
139
|
+
hasExitStyles: hasExitStyles,
|
|
140
|
+
isAnimationEnabled: isAnimationEnabled,
|
|
141
|
+
isExiting: isExiting,
|
|
112
142
|
truncateText: truncateText,
|
|
113
143
|
children: jsxs(SelectPrimitive.Root, {
|
|
114
144
|
...selectProps,
|
|
@@ -150,7 +180,9 @@ const Trigger = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
150
180
|
...triggerProps
|
|
151
181
|
} = props;
|
|
152
182
|
const {
|
|
153
|
-
getLeftElement
|
|
183
|
+
getLeftElement,
|
|
184
|
+
hasExitStyles,
|
|
185
|
+
isAnimationEnabled
|
|
154
186
|
} = useSelectContext();
|
|
155
187
|
const selectedLeftElement = value ? getLeftElement(value) : null;
|
|
156
188
|
const shouldShowPlaceholderLeftElement = !value && placeholderLeftElement;
|
|
@@ -166,6 +198,9 @@ const Trigger = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
166
198
|
[styles.error]: hasError,
|
|
167
199
|
[styles.readonly]: readonly
|
|
168
200
|
}),
|
|
201
|
+
...(isAnimationEnabled && hasExitStyles && {
|
|
202
|
+
'data-bp-exiting': 'true'
|
|
203
|
+
}),
|
|
169
204
|
id: selectId,
|
|
170
205
|
children: [jsxs("span", {
|
|
171
206
|
className: styles.triggerValue,
|
|
@@ -189,21 +224,34 @@ const Content = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
189
224
|
children,
|
|
190
225
|
className,
|
|
191
226
|
container,
|
|
227
|
+
onAnimationEnd: onAnimationEndProp,
|
|
192
228
|
...contentProps
|
|
193
229
|
} = props;
|
|
194
230
|
const {
|
|
195
231
|
enableModernizedComponents
|
|
196
232
|
} = useBlueprintModernization();
|
|
197
233
|
const {
|
|
234
|
+
completeExit,
|
|
235
|
+
hasExitStyles,
|
|
236
|
+
isAnimationEnabled,
|
|
237
|
+
isExiting,
|
|
198
238
|
truncateText
|
|
199
239
|
} = useSelectContext();
|
|
240
|
+
const handleExitAnimationEvent = useCallback(event => {
|
|
241
|
+
handleSelectContentExitAnimationEnd(event, isExiting, completeExit);
|
|
242
|
+
}, [completeExit, isExiting]);
|
|
200
243
|
return jsx(SelectPrimitive.Portal, {
|
|
201
244
|
container: container,
|
|
202
245
|
children: jsx(SelectPrimitive.Content, {
|
|
203
246
|
...contentProps,
|
|
204
247
|
ref: forwardedRef,
|
|
205
248
|
className: clsx(styles.content, className),
|
|
249
|
+
"data-bp-animated": isAnimationEnabled ? 'true' : 'false',
|
|
250
|
+
"data-bp-exiting": hasExitStyles ? 'true' : 'false',
|
|
206
251
|
"data-modern": enableModernizedComponents ? 'true' : 'false',
|
|
252
|
+
onAnimationEnd: composeEventHandlers(onAnimationEndProp, handleExitAnimationEvent, {
|
|
253
|
+
checkForDefaultPrevented: false
|
|
254
|
+
}),
|
|
207
255
|
position: "popper",
|
|
208
256
|
sideOffset: 8,
|
|
209
257
|
children: jsx(ScrollArea.Root, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import '../index.css';
|
|
2
|
-
var styles = {"container":"bp_select_module_container--
|
|
2
|
+
var styles = {"container":"bp_select_module_container--80de0","label":"bp_select_module_label--80de0","triggerBtn":"bp_select_module_triggerBtn--80de0","ellipsis":"bp_select_module_ellipsis--80de0","disabled":"bp_select_module_disabled--80de0","triggerValue":"bp_select_module_triggerValue--80de0","triggerLeftElement":"bp_select_module_triggerLeftElement--80de0","readonly":"bp_select_module_readonly--80de0","error":"bp_select_module_error--80de0","iconWrapper":"bp_select_module_iconWrapper--80de0","iconCaret":"bp_select_module_iconCaret--80de0","iconPencilCrossed":"bp_select_module_iconPencilCrossed--80de0","content":"bp_select_module_content--80de0","viewport":"bp_select_module_viewport--80de0","option":"bp_select_module_option--80de0","secondaryText":"bp_select_module_secondaryText--80de0","textWrapper":"bp_select_module_textWrapper--80de0","leftElement":"bp_select_module_leftElement--80de0","hasLeftElement":"bp_select_module_hasLeftElement--80de0","indicator":"bp_select_module_indicator--80de0","separator":"bp_select_module_separator--80de0","withTruncatedText":"bp_select_module_withTruncatedText--80de0"};
|
|
3
3
|
|
|
4
4
|
export { styles as default };
|
|
@@ -120,6 +120,10 @@ export interface SelectContentProps extends React.ComponentPropsWithRef<'div'> {
|
|
|
120
120
|
* Can be prevented.
|
|
121
121
|
*/
|
|
122
122
|
onEscapeKeyDown?: RadixSelectContentProps['onEscapeKeyDown'];
|
|
123
|
+
/**
|
|
124
|
+
* Event handler called when a CSS animation on the content ends.
|
|
125
|
+
*/
|
|
126
|
+
onAnimationEnd?: React.AnimationEventHandler<HTMLDivElement>;
|
|
123
127
|
}
|
|
124
128
|
export interface SelectOptionProps extends React.ComponentPropsWithRef<'div'> {
|
|
125
129
|
/**
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AnimationEvent, type Dispatch, type SetStateAction } from 'react';
|
|
2
|
+
type SetOpen = Dispatch<SetStateAction<boolean | undefined>>;
|
|
3
|
+
/**
|
|
4
|
+
* Deferred close for Select menu exit animations on Radix Select 2.0.x, which unmounts
|
|
5
|
+
* content immediately without `@radix-ui/react-presence`. Keeps Radix `open` true while
|
|
6
|
+
* exit CSS runs, then commits the close on `animationend`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useSelectExitAnimation({ open, setOpen, isAnimationEnabled, }: {
|
|
9
|
+
open: boolean;
|
|
10
|
+
setOpen: SetOpen;
|
|
11
|
+
isAnimationEnabled: boolean;
|
|
12
|
+
}): {
|
|
13
|
+
radixOpen: boolean;
|
|
14
|
+
isExiting: boolean;
|
|
15
|
+
hasExitStyles: boolean;
|
|
16
|
+
handleOpenChange: (nextOpen: boolean) => void;
|
|
17
|
+
completeExit: () => void;
|
|
18
|
+
};
|
|
19
|
+
export declare function handleSelectContentExitAnimationEnd(event: AnimationEvent<HTMLDivElement>, isExiting: boolean, completeExit: () => void): void;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import { bpDurationShort } from '@box/blueprint-web-assets/tokens/tokens';
|
|
3
|
+
import { noop } from '../../utils/noop.js';
|
|
4
|
+
|
|
5
|
+
const completeExitNoop = noop;
|
|
6
|
+
const EXIT_DURATION_MS = Number.parseFloat(bpDurationShort);
|
|
7
|
+
const SELECT_EXIT_ANIMATION_FALLBACK_MS = EXIT_DURATION_MS * 3;
|
|
8
|
+
/**
|
|
9
|
+
* Deferred close for Select menu exit animations on Radix Select 2.0.x, which unmounts
|
|
10
|
+
* content immediately without `@radix-ui/react-presence`. Keeps Radix `open` true while
|
|
11
|
+
* exit CSS runs, then commits the close on `animationend`.
|
|
12
|
+
*/
|
|
13
|
+
function useSelectExitAnimation({
|
|
14
|
+
open,
|
|
15
|
+
setOpen,
|
|
16
|
+
isAnimationEnabled
|
|
17
|
+
}) {
|
|
18
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
19
|
+
// Keep exit styles applied through the final unmount frame to avoid a visible flash.
|
|
20
|
+
const [hasExitStyles, setHasExitStyles] = useState(false);
|
|
21
|
+
const exitFallbackTimerRef = useRef(null);
|
|
22
|
+
const exitCommitRafRef = useRef(null);
|
|
23
|
+
const isExitingRef = useRef(isExiting);
|
|
24
|
+
isExitingRef.current = isExiting;
|
|
25
|
+
const clearExitFallbackTimer = useCallback(() => {
|
|
26
|
+
if (exitFallbackTimerRef.current !== null) {
|
|
27
|
+
clearTimeout(exitFallbackTimerRef.current);
|
|
28
|
+
exitFallbackTimerRef.current = null;
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
const cancelExitCommitRaf = useCallback(() => {
|
|
32
|
+
if (exitCommitRafRef.current !== null) {
|
|
33
|
+
cancelAnimationFrame(exitCommitRafRef.current);
|
|
34
|
+
exitCommitRafRef.current = null;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}, []);
|
|
39
|
+
const clearExitState = useCallback(() => {
|
|
40
|
+
clearExitFallbackTimer();
|
|
41
|
+
cancelExitCommitRaf();
|
|
42
|
+
isExitingRef.current = false;
|
|
43
|
+
setIsExiting(false);
|
|
44
|
+
setHasExitStyles(false);
|
|
45
|
+
}, [cancelExitCommitRaf, clearExitFallbackTimer]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!isAnimationEnabled) {
|
|
48
|
+
const wasExiting = isExitingRef.current;
|
|
49
|
+
const hadPendingCommit = exitCommitRafRef.current !== null;
|
|
50
|
+
clearExitState();
|
|
51
|
+
if (wasExiting || hadPendingCommit) {
|
|
52
|
+
setOpen(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, [clearExitState, isAnimationEnabled, setOpen]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!open && isExiting) {
|
|
58
|
+
clearExitState();
|
|
59
|
+
}
|
|
60
|
+
}, [clearExitState, isExiting, open]);
|
|
61
|
+
useEffect(() => () => {
|
|
62
|
+
clearExitFallbackTimer();
|
|
63
|
+
cancelExitCommitRaf();
|
|
64
|
+
}, [cancelExitCommitRaf, clearExitFallbackTimer]);
|
|
65
|
+
const handleOpenChange = useCallback(nextOpen => {
|
|
66
|
+
if (!isAnimationEnabled) {
|
|
67
|
+
setOpen(nextOpen);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (nextOpen) {
|
|
71
|
+
clearExitState();
|
|
72
|
+
setOpen(true);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (isExiting) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (open) {
|
|
79
|
+
setIsExiting(true);
|
|
80
|
+
setHasExitStyles(true);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
setIsExiting(false);
|
|
84
|
+
setHasExitStyles(false);
|
|
85
|
+
setOpen(false);
|
|
86
|
+
}, [clearExitState, isAnimationEnabled, isExiting, open, setOpen]);
|
|
87
|
+
const completeExit = useCallback(() => {
|
|
88
|
+
if (!isAnimationEnabled || !isExitingRef.current) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
clearExitFallbackTimer();
|
|
92
|
+
cancelExitCommitRaf();
|
|
93
|
+
isExitingRef.current = false;
|
|
94
|
+
setIsExiting(false);
|
|
95
|
+
// Commit close on the next frame so exit styles (opacity/transform) paint before Radix unmounts.
|
|
96
|
+
exitCommitRafRef.current = requestAnimationFrame(() => {
|
|
97
|
+
exitCommitRafRef.current = null;
|
|
98
|
+
setOpen(false);
|
|
99
|
+
setHasExitStyles(false);
|
|
100
|
+
});
|
|
101
|
+
}, [cancelExitCommitRaf, clearExitFallbackTimer, isAnimationEnabled, setOpen]);
|
|
102
|
+
const completeExitRef = useRef(completeExit);
|
|
103
|
+
completeExitRef.current = completeExit;
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!isAnimationEnabled || !isExiting) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
clearExitFallbackTimer();
|
|
109
|
+
exitFallbackTimerRef.current = setTimeout(() => {
|
|
110
|
+
exitFallbackTimerRef.current = null;
|
|
111
|
+
completeExitRef.current();
|
|
112
|
+
}, SELECT_EXIT_ANIMATION_FALLBACK_MS);
|
|
113
|
+
return clearExitFallbackTimer;
|
|
114
|
+
}, [clearExitFallbackTimer, isAnimationEnabled, isExiting]);
|
|
115
|
+
if (!isAnimationEnabled) {
|
|
116
|
+
return {
|
|
117
|
+
radixOpen: open,
|
|
118
|
+
isExiting: false,
|
|
119
|
+
hasExitStyles: false,
|
|
120
|
+
handleOpenChange: setOpen,
|
|
121
|
+
completeExit: completeExitNoop
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
radixOpen: isExiting ? true : open,
|
|
126
|
+
isExiting,
|
|
127
|
+
hasExitStyles,
|
|
128
|
+
handleOpenChange,
|
|
129
|
+
completeExit
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function handleSelectContentExitAnimationEnd(event, isExiting, completeExit) {
|
|
133
|
+
if (event.target !== event.currentTarget || !isExiting || event.type === 'animationcancel') {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const {
|
|
137
|
+
getAnimations
|
|
138
|
+
} = event.currentTarget;
|
|
139
|
+
if (!getAnimations) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const animations = getAnimations.call(event.currentTarget);
|
|
143
|
+
if (animations.length === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const hasRunningAnimations = animations.some(animation => animation.playState !== 'finished');
|
|
147
|
+
if (hasRunningAnimations) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
completeExit();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { handleSelectContentExitAnimationEnd, useSelectExitAnimation };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@box/blueprint-web",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.13.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@ariakit/react": "0.4.21",
|
|
52
52
|
"@ariakit/react-core": "0.4.21",
|
|
53
|
-
"@box/blueprint-web-assets": "^5.5.
|
|
53
|
+
"@box/blueprint-web-assets": "^5.5.10",
|
|
54
54
|
"@internationalized/date": "^3.12.0",
|
|
55
55
|
"@radix-ui/react-accordion": "1.1.2",
|
|
56
56
|
"@radix-ui/react-checkbox": "1.0.4",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"type-fest": "^3.2.0"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@box/storybook-utils": "^1.1.
|
|
82
|
+
"@box/storybook-utils": "^1.1.34",
|
|
83
83
|
"@figma/code-connect": "1.4.4",
|
|
84
84
|
"@types/react": "^18.0.0",
|
|
85
85
|
"@types/react-dom": "^18.0.0",
|