@box/blueprint-web 17.3.0 → 17.4.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.
@@ -2,9 +2,10 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { XMark } from '@box/blueprint-web-assets/icons/Fill';
3
3
  import { Size4 } from '@box/blueprint-web-assets/tokens/tokens';
4
4
  import clsx from 'clsx';
5
- import React__default, { forwardRef } from 'react';
5
+ import React__default, { forwardRef, useMemo, useCallback } from 'react';
6
6
  import { Text } from '../text/text.js';
7
7
  import styles from './input-chip.module.js';
8
+ import { useInputChipPresence } from './utils/use-input-chip-presence.js';
8
9
 
9
10
  const getVariantClass = variant => {
10
11
  if (variant === 'error') {
@@ -20,12 +21,32 @@ const InputChip = /*#__PURE__*/forwardRef((props, ref) => {
20
21
  const {
21
22
  avatar: avatarProp,
22
23
  label,
24
+ tooltip,
23
25
  variant,
24
26
  className,
25
27
  onDelete,
28
+ present = true,
29
+ isAnimationEnabled = false,
30
+ onAnimationEnd: onAnimationEndProp,
31
+ 'aria-hidden': ariaHiddenProp,
32
+ tabIndex: tabIndexProp,
26
33
  ...buttonProps
27
34
  } = props;
28
35
  const enableModernizedComponents = true;
36
+ const content = useMemo(() => ({
37
+ avatar: avatarProp,
38
+ label,
39
+ tooltip,
40
+ variant
41
+ }), [avatarProp, label, tooltip, variant]);
42
+ const presence = useInputChipPresence(present, isAnimationEnabled, content);
43
+ const {
44
+ onAnimationEnd: onPresenceAnimationEnd
45
+ } = presence;
46
+ const handleAnimationEnd = useCallback(event => {
47
+ onPresenceAnimationEnd(event);
48
+ onAnimationEndProp?.(event);
49
+ }, [onPresenceAnimationEnd, onAnimationEndProp]);
29
50
  // Clone avatar element to override its props
30
51
  const withSmallAvatarSize = element => {
31
52
  const cloneAvatarWithSmallSize = el => /*#__PURE__*/React__default.cloneElement(el, {
@@ -48,29 +69,44 @@ const InputChip = /*#__PURE__*/forwardRef((props, ref) => {
48
69
  // Avatar without an IconBadge, apply size directly to the Avatar
49
70
  return cloneAvatarWithSmallSize(element);
50
71
  };
51
- const avatar = avatarProp ? withSmallAvatarSize(avatarProp) : null;
72
+ if (!presence.isMounted) {
73
+ return null;
74
+ }
75
+ const {
76
+ avatar: chipAvatar,
77
+ label: chipLabel,
78
+ variant: chipVariant
79
+ } = presence.content;
80
+ const isExiting = presence.presenceState === 'closed';
81
+ const canDelete = Boolean(onDelete) && !isExiting;
82
+ const avatar = chipAvatar ? withSmallAvatarSize(chipAvatar) : null;
52
83
  const handleKeyDown = event => {
53
- if (onDelete && DELETE_TRIGGER_KEYS.includes(event.code)) {
54
- onDelete();
84
+ if (canDelete && DELETE_TRIGGER_KEYS.includes(event.code)) {
85
+ onDelete?.();
55
86
  }
56
87
  buttonProps.onKeyDown?.(event);
57
88
  };
58
- const hasError = variant === 'error';
59
- const variantClass = getVariantClass(variant);
60
- const containerClassName = clsx(styles.container, variantClass, avatar && styles.avatarContainer, onDelete && styles.deleteButton, !onDelete && enableModernizedComponents && styles.noDeleteButton, className);
89
+ const hasError = chipVariant === 'error';
90
+ const variantClass = getVariantClass(chipVariant);
91
+ const containerClassName = clsx(styles.container, variantClass, avatar && styles.avatarContainer, canDelete && styles.deleteButton, !canDelete && enableModernizedComponents && styles.noDeleteButton, className);
61
92
  return /* eslint-disable-next-line jsx-a11y/role-supports-aria-props */jsxs("button", {
62
93
  ...buttonProps,
63
94
  ref: ref,
95
+ "aria-hidden": isExiting ? true : ariaHiddenProp,
64
96
  "aria-invalid": hasError,
65
97
  className: containerClassName,
98
+ "data-bp-animated": isAnimationEnabled ? 'true' : 'false',
66
99
  "data-modern": 'true' ,
100
+ "data-state": presence.presenceState,
101
+ onAnimationEnd: handleAnimationEnd,
67
102
  onKeyDown: handleKeyDown,
103
+ tabIndex: isExiting ? -1 : tabIndexProp,
68
104
  type: "button",
69
105
  children: [avatar, jsx(Text, {
70
106
  as: "span",
71
107
  className: styles.label,
72
- children: label
73
- }), onDelete && (/* Don't use `onClick` handler on icon due to BUIE's "pointer-events: none" style that would cause click event to not trigger */
108
+ children: chipLabel
109
+ }), canDelete && (/* Don't use `onClick` handler on icon due to BUIE's "pointer-events: none" style that would cause click event to not trigger */
74
110
  /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
75
111
  jsx("span", {
76
112
  className: styles.deleteIcon,
@@ -1,4 +1,4 @@
1
1
  import '../index.css';
2
- var styles = {"container":"bp_input_chip_module_container--5c79b","avatarContainer":"bp_input_chip_module_avatarContainer--5c79b","deleteButton":"bp_input_chip_module_deleteButton--5c79b","noDeleteButton":"bp_input_chip_module_noDeleteButton--5c79b","error":"bp_input_chip_module_error--5c79b","deleteIcon":"bp_input_chip_module_deleteIcon--5c79b","label":"bp_input_chip_module_label--5c79b","variable":"bp_input_chip_module_variable--5c79b","avatar":"bp_input_chip_module_avatar--5c79b","modern":"bp_input_chip_module_modern--5c79b"};
2
+ var styles = {"container":"bp_input_chip_module_container--d98d0","avatarContainer":"bp_input_chip_module_avatarContainer--d98d0","deleteButton":"bp_input_chip_module_deleteButton--d98d0","noDeleteButton":"bp_input_chip_module_noDeleteButton--d98d0","error":"bp_input_chip_module_error--d98d0","deleteIcon":"bp_input_chip_module_deleteIcon--d98d0","label":"bp_input_chip_module_label--d98d0","variable":"bp_input_chip_module_variable--d98d0","avatar":"bp_input_chip_module_avatar--d98d0","modern":"bp_input_chip_module_modern--d98d0"};
3
3
 
4
4
  export { styles as default };
@@ -22,4 +22,20 @@ export interface InputChipProps extends HTMLAttributes<HTMLElement> {
22
22
  * Callback fired when chip is deleted with X button click or keyboard interaction.
23
23
  */
24
24
  onDelete?: () => void;
25
+ /**
26
+ * Whether the chip should be shown. Set to `false` to trigger the exit animation (when
27
+ * `isAnimationEnabled` is `true`); the chip keeps rendering (non-interactively) until the exit
28
+ * animation completes, then unmounts itself. When `isAnimationEnabled` is `false`,
29
+ * `present={false}` unmounts immediately.
30
+ *
31
+ * @default true
32
+ */
33
+ present?: boolean;
34
+ /**
35
+ * Opts the chip's mount/unmount transition into the Blueprint animation system. When `false`
36
+ * (default), `present` changes take effect immediately with no animation.
37
+ *
38
+ * @default false
39
+ */
40
+ isAnimationEnabled?: boolean;
25
41
  }
@@ -0,0 +1,24 @@
1
+ import { type AnimationEvent } from 'react';
2
+ import { type InputChipProps } from '../types';
3
+ export interface InputChipContentSnapshot {
4
+ avatar?: InputChipProps['avatar'];
5
+ label: string;
6
+ tooltip?: InputChipProps['tooltip'];
7
+ variant?: InputChipProps['variant'];
8
+ }
9
+ type PresenceState = 'open' | 'closed';
10
+ /**
11
+ * Exit-animation presence for `InputChip`. Pattern mirrors `useInlineErrorPresence`: keep the
12
+ * chip mounted (non-interactively) while its exit CSS animation runs, then unmount on
13
+ * `animationend`. Also freezes `content` the moment `present` becomes `false`, so a caller (e.g.
14
+ * Combobox) doesn't need its own cache of a chip's display data once it no longer has a source of
15
+ * truth for it — as long as this InputChip element stays mounted at a stable tree position across
16
+ * the transition (see ChipsGroup).
17
+ */
18
+ export declare function useInputChipPresence(present: boolean, isAnimationEnabled: boolean, content: InputChipContentSnapshot): {
19
+ content: InputChipContentSnapshot;
20
+ isMounted: boolean;
21
+ presenceState: PresenceState | undefined;
22
+ onAnimationEnd: (e: AnimationEvent<HTMLButtonElement>) => void;
23
+ };
24
+ export {};
@@ -0,0 +1,54 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { noop } from '../../utils/noop.js';
3
+ import { useEnhancedEffect } from '../../utils/useEnhancedEffect.js';
4
+
5
+ const animationEndNoop = noop;
6
+ /**
7
+ * Exit-animation presence for `InputChip`. Pattern mirrors `useInlineErrorPresence`: keep the
8
+ * chip mounted (non-interactively) while its exit CSS animation runs, then unmount on
9
+ * `animationend`. Also freezes `content` the moment `present` becomes `false`, so a caller (e.g.
10
+ * Combobox) doesn't need its own cache of a chip's display data once it no longer has a source of
11
+ * truth for it — as long as this InputChip element stays mounted at a stable tree position across
12
+ * the transition (see ChipsGroup).
13
+ */
14
+ function useInputChipPresence(present, isAnimationEnabled, content) {
15
+ const [renderedContent, setRenderedContent] = useState(content);
16
+ const [presenceState, setPresenceState] = useState(() => isAnimationEnabled && present ? 'open' : undefined);
17
+ const isMounted = isAnimationEnabled && (presenceState === 'open' || presenceState === 'closed');
18
+ const unmountAfterExit = useCallback(() => setPresenceState(undefined), []);
19
+ const onAnimationEnd = useCallback(e => {
20
+ if (e.target !== e.currentTarget || presenceState !== 'closed') {
21
+ return;
22
+ }
23
+ unmountAfterExit();
24
+ }, [presenceState, unmountAfterExit]);
25
+ useEnhancedEffect(() => {
26
+ if (!isAnimationEnabled) {
27
+ return;
28
+ }
29
+ if (present) {
30
+ setRenderedContent(content);
31
+ setPresenceState('open');
32
+ return;
33
+ }
34
+ // Don't reassign renderedContent here — freezes it at its last "present" value.
35
+ setPresenceState(current => current === 'open' ? 'closed' : current);
36
+ }, [content, isAnimationEnabled, present]);
37
+ if (!isAnimationEnabled) {
38
+ // Legacy / animation-disabled path: no bookkeeping, no lingering DOM node.
39
+ return {
40
+ content,
41
+ isMounted: present,
42
+ presenceState: undefined,
43
+ onAnimationEnd: animationEndNoop
44
+ };
45
+ }
46
+ return {
47
+ content: renderedContent,
48
+ isMounted,
49
+ presenceState,
50
+ onAnimationEnd
51
+ };
52
+ }
53
+
54
+ export { useInputChipPresence };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@box/blueprint-web",
3
- "version": "17.3.0",
3
+ "version": "17.4.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.9.1",
58
+ "@box/blueprint-web-assets": "^5.9.3",
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",
@@ -86,7 +86,7 @@
86
86
  "devDependencies": {
87
87
  "@box/box-test-client": "^3.2.7",
88
88
  "@box/playwright-utils": "^2.8.5",
89
- "@box/storybook-utils": "^1.2.32",
89
+ "@box/storybook-utils": "^1.2.34",
90
90
  "@figma/code-connect": "1.4.4",
91
91
  "@playwright/test": "1.46.1",
92
92
  "@types/js-yaml": "^4.0.9",