@box/blueprint-web 15.2.0 → 15.3.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.
@@ -38,7 +38,7 @@ export declare const BLUEPRINT_CONFIGURATION_SPLITS: {
38
38
  */
39
39
  export declare const ANIMATED_COMPONENTS_BY_PHASE: {
40
40
  readonly phase1: readonly ["Button", "IconButton", "DropdownMenu", "Tooltip", "Popover", "SplitButton", "CardTooltip", "NavigationMenu", "Modal", "TextButton", "ContextMenu"];
41
- readonly phase2: readonly ["DropdownTrigger", "Switch", "Checkbox", "TextInput", "Select", "Datepicker", "TextArea", "Combobox", "PasswordInput", "ComboboxGroup"];
41
+ readonly phase2: readonly ["DropdownTrigger", "Switch", "Checkbox", "TextInput", "Select", "Datepicker", "TextArea", "Combobox", "PasswordInput", "ComboboxGroup", "InlineError"];
42
42
  readonly phase3: readonly [];
43
43
  };
44
44
  /**
@@ -40,7 +40,7 @@ const BLUEPRINT_CONFIGURATION_SPLITS = deepFreeze({
40
40
  */
41
41
  const ANIMATED_COMPONENTS_BY_PHASE = deepFreeze({
42
42
  phase1: ['Button', 'IconButton', 'DropdownMenu', 'Tooltip', 'Popover', 'SplitButton', 'CardTooltip', 'NavigationMenu', 'Modal', 'TextButton', 'ContextMenu'],
43
- phase2: ['DropdownTrigger', 'Switch', 'Checkbox', 'TextInput', 'Select', 'Datepicker', 'TextArea', 'Combobox', 'PasswordInput', 'ComboboxGroup'],
43
+ phase2: ['DropdownTrigger', 'Switch', 'Checkbox', 'TextInput', 'Select', 'Datepicker', 'TextArea', 'Combobox', 'PasswordInput', 'ComboboxGroup', 'InlineError'],
44
44
  phase3: []
45
45
  });
46
46
  /**
@@ -4546,7 +4546,7 @@
4546
4546
  .bp_input_chip_module_container--8ac7b .bp_input_chip_module_avatar--8ac7b.bp_input_chip_module_modern--8ac7b *{
4547
4547
  border-radius:unset;
4548
4548
  }
4549
- .bp_inline_error_module_inlineError--4733a[data-modern=false]{
4549
+ .bp_inline_error_module_inlineError--e70b6[data-modern=false]{
4550
4550
  --inline-error-gap:var(--size-1);
4551
4551
  --inline-error-height:var(--size-4);
4552
4552
  --inline-error-text-color:var(--text-text-error-on-light);
@@ -4559,10 +4559,11 @@
4559
4559
  text-transform:var(--body-default-bold-text-case);
4560
4560
  }
4561
4561
 
4562
- .bp_inline_error_module_inlineError--4733a[data-modern=true]{
4562
+ .bp_inline_error_module_inlineError--e70b6[data-modern=true]{
4563
4563
  --inline-error-gap:var(--bp-space-010);
4564
4564
  --inline-error-height:var(--bp-size-040);
4565
4565
  --inline-error-text-color:var(--bp-text-text-error-on-light);
4566
+ --inline-error-slide-offset:calc(var(--bp-font-size-02, 0.625rem)*-1);
4566
4567
  font-family:var(--bp-font-font-family), -apple-system, BlinkMacSystemFont, "San Francisco", "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
4567
4568
  font-size:var(--bp-font-size-05);
4568
4569
  font-style:normal;
@@ -4571,21 +4572,54 @@
4571
4572
  line-height:var(--bp-font-line-height-04);
4572
4573
  }
4573
4574
 
4574
- .bp_inline_error_module_inlineError--4733a.bp_inline_error_module_inlineError--4733a{
4575
+ .bp_inline_error_module_inlineError--e70b6.bp_inline_error_module_inlineError--e70b6{
4575
4576
  align-items:center;
4576
4577
  color:var(--inline-error-text-color);
4577
4578
  display:flex;
4578
4579
  gap:var(--inline-error-gap);
4579
4580
  line-height:var(--inline-error-height);
4580
4581
  }
4581
- .bp_inline_error_module_inlineError--4733a.bp_inline_error_module_inlineError--4733a.bp_inline_error_module_empty--4733a{
4582
+ .bp_inline_error_module_inlineError--e70b6.bp_inline_error_module_inlineError--e70b6.bp_inline_error_module_empty--e70b6{
4582
4583
  display:none;
4583
4584
  }
4584
- .bp_inline_error_module_inlineError--4733a.bp_inline_error_module_inlineError--4733a .bp_inline_error_module_errorIcon--4733a{
4585
+ .bp_inline_error_module_inlineError--e70b6.bp_inline_error_module_inlineError--e70b6 .bp_inline_error_module_errorIcon--e70b6{
4585
4586
  align-self:flex-start;
4586
4587
  flex-grow:0;
4587
4588
  flex-shrink:0;
4588
4589
  }
4590
+
4591
+ .bp_inline_error_module_inlineError--e70b6.bp_inline_error_module_inlineError--e70b6.bp_inline_error_module_animated--e70b6[data-state=open]{
4592
+ animation-duration:var(--bp-duration-short);
4593
+ animation-fill-mode:both;
4594
+ animation-name:bp_inline_error_module_bpInlineErrorSlideEnter--e70b6;
4595
+ animation-timing-function:var(--bp-curve-small-on);
4596
+ }
4597
+
4598
+ .bp_inline_error_module_inlineError--e70b6.bp_inline_error_module_inlineError--e70b6.bp_inline_error_module_animated--e70b6[data-state=closed]{
4599
+ animation-duration:var(--bp-duration-short);
4600
+ animation-fill-mode:forwards;
4601
+ animation-name:bp_inline_error_module_bpInlineErrorFadeExit--e70b6;
4602
+ animation-timing-function:var(--bp-curve-small-off);
4603
+ }
4604
+
4605
+ @keyframes bp_inline_error_module_bpInlineErrorSlideEnter--e70b6{
4606
+ from{
4607
+ opacity:var(--bp-opacity-hidden);
4608
+ transform:translateY(var(--inline-error-slide-offset));
4609
+ }
4610
+ to{
4611
+ opacity:var(--bp-opacity-visible);
4612
+ transform:translateY(0);
4613
+ }
4614
+ }
4615
+ @keyframes bp_inline_error_module_bpInlineErrorFadeExit--e70b6{
4616
+ from{
4617
+ opacity:var(--bp-opacity-visible);
4618
+ }
4619
+ to{
4620
+ opacity:var(--bp-opacity-hidden);
4621
+ }
4622
+ }
4589
4623
  .bp_labelable_module_required--25dbc{
4590
4624
  align-items:center;
4591
4625
  display:inline-flex;
@@ -3,44 +3,82 @@ import { AlertBadge } from '@box/blueprint-web-assets/icons/Fill';
3
3
  import { AlertCircle } from '@box/blueprint-web-assets/icons/Medium';
4
4
  import { Size4, IconIconErrorOnLight, bpSize050 } from '@box/blueprint-web-assets/tokens/tokens';
5
5
  import clsx from 'clsx';
6
- import { forwardRef } from 'react';
6
+ import { forwardRef, useCallback } from 'react';
7
+ import '../../blueprint-configuration-context/blueprint-configuration-context.js';
8
+ import '../../blueprint-configuration-context/consts.js';
9
+ import { useBlueprintConfiguration } from '../../blueprint-configuration-context/useBlueprintConfiguration.js';
7
10
  import { useBlueprintModernization } from '../../blueprint-modernization-context/useBlueprintModernization.js';
11
+ import { useInlineErrorPresence } from './utils/use-inline-error-presence.js';
8
12
  import styles from './inline-error.module.js';
9
13
 
14
+ const renderErrorContent = (content, enableModernizedComponents, errorIconClassName) => content && (!enableModernizedComponents ? jsxs(Fragment, {
15
+ children: [jsx(AlertBadge, {
16
+ className: errorIconClassName,
17
+ color: IconIconErrorOnLight,
18
+ height: Size4,
19
+ role: "presentation",
20
+ width: Size4
21
+ }), content]
22
+ }) : jsxs(Fragment, {
23
+ children: [jsx(AlertCircle, {
24
+ className: errorIconClassName,
25
+ color: IconIconErrorOnLight,
26
+ height: bpSize050,
27
+ role: "presentation",
28
+ width: bpSize050
29
+ }), content]
30
+ }));
10
31
  /** Renders an inline error message and icon, used to show error state in form elements. */
11
32
  const InlineError = /*#__PURE__*/forwardRef((props, forwardedRef) => {
12
33
  const {
13
34
  children,
14
35
  className,
36
+ onAnimationEnd: onAnimationEndProp,
15
37
  ...rest
16
38
  } = props;
17
39
  const {
18
40
  enableModernizedComponents
19
41
  } = useBlueprintModernization();
42
+ const {
43
+ componentsWithAnimationEnabled
44
+ } = useBlueprintConfiguration();
45
+ const isAnimationEnabled = componentsWithAnimationEnabled.includes('InlineError');
46
+ const useModernizedAnimation = isAnimationEnabled && enableModernizedComponents;
47
+ const presence = useInlineErrorPresence(children, useModernizedAnimation);
48
+ const {
49
+ onAnimationEnd: onPresenceAnimationEnd
50
+ } = presence;
51
+ const handleAnimationEnd = useCallback(event => {
52
+ onPresenceAnimationEnd(event);
53
+ onAnimationEndProp?.(event);
54
+ }, [onPresenceAnimationEnd, onAnimationEndProp]);
55
+ /** Legacy path: identical DOM/CSS to pre-animation InlineError (no mount/unmount, `.empty` + `display: none`). */
56
+ if (!useModernizedAnimation) {
57
+ return jsx("span", {
58
+ ...rest,
59
+ ref: forwardedRef,
60
+ className: clsx([className, styles.inlineError, {
61
+ [styles.empty]: !children
62
+ }]),
63
+ "data-modern": enableModernizedComponents ? 'true' : 'false',
64
+ onAnimationEnd: onAnimationEndProp,
65
+ children: renderErrorContent(children, enableModernizedComponents, styles.errorIcon)
66
+ });
67
+ }
68
+ /** `return null` when there is no error (and we are not in the exit phase): no visible slot for the
69
+ message — layout matches legacy (legacy hides the same slot via `.empty` + `display: none`). */
70
+ if (!presence.isMounted) {
71
+ return null;
72
+ }
20
73
  return jsx("span", {
21
74
  ...rest,
22
75
  ref: forwardedRef,
23
- className: clsx([className, styles.inlineError, {
24
- [styles.empty]: !children
25
- }]),
26
- "data-modern": enableModernizedComponents ? 'true' : 'false',
27
- children: children && (!enableModernizedComponents ? jsxs(Fragment, {
28
- children: [jsx(AlertBadge, {
29
- className: styles.errorIcon,
30
- color: IconIconErrorOnLight,
31
- height: Size4,
32
- role: "presentation",
33
- width: Size4
34
- }), children]
35
- }) : jsxs(Fragment, {
36
- children: [jsx(AlertCircle, {
37
- className: styles.errorIcon,
38
- color: IconIconErrorOnLight,
39
- height: bpSize050,
40
- role: "presentation",
41
- width: bpSize050
42
- }), children]
43
- }))
76
+ className: clsx([className, styles.inlineError, styles.animated]),
77
+ "data-bp-animated": "true",
78
+ "data-modern": "true",
79
+ "data-state": presence.presenceState,
80
+ onAnimationEnd: handleAnimationEnd,
81
+ children: renderErrorContent(presence.content, enableModernizedComponents, styles.errorIcon)
44
82
  });
45
83
  });
46
84
 
@@ -1,4 +1,4 @@
1
1
  import '../../index.css';
2
- var styles = {"inlineError":"bp_inline_error_module_inlineError--4733a","empty":"bp_inline_error_module_empty--4733a","errorIcon":"bp_inline_error_module_errorIcon--4733a"};
2
+ var styles = {"inlineError":"bp_inline_error_module_inlineError--e70b6","empty":"bp_inline_error_module_empty--e70b6","errorIcon":"bp_inline_error_module_errorIcon--e70b6","animated":"bp_inline_error_module_animated--e70b6"};
3
3
 
4
4
  export { styles as default };
@@ -0,0 +1,13 @@
1
+ import { type AnimationEvent } from 'react';
2
+ type PresenceState = 'open' | 'closed';
3
+ /**
4
+ * Exit-animation presence for `InlineError`. Pattern inspired by Radix UI `usePresence`:
5
+ * keep the node mounted while exit CSS runs, then unmount on `animationend`.
6
+ */
7
+ export declare function useInlineErrorPresence(children: React.ReactNode, isAnimationEnabled: boolean): {
8
+ content: string | number | boolean | import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | import("react").ReactPortal | null | undefined;
9
+ isMounted: boolean;
10
+ presenceState: PresenceState | undefined;
11
+ onAnimationEnd: (e: AnimationEvent<HTMLSpanElement>) => void;
12
+ };
13
+ export {};
@@ -0,0 +1,54 @@
1
+ import noop from 'lodash/noop';
2
+ import { useState, useCallback } from 'react';
3
+ import { useEnhancedEffect } from '../../../utils/useEnhancedEffect.js';
4
+
5
+ const animationEndNoop = noop;
6
+ /**
7
+ * Exit-animation presence for `InlineError`. Pattern inspired by Radix UI `usePresence`:
8
+ * keep the node mounted while exit CSS runs, then unmount on `animationend`.
9
+ */
10
+ function useInlineErrorPresence(children, isAnimationEnabled) {
11
+ const hasContent = Boolean(children);
12
+ const [renderedChildren, setRenderedChildren] = useState(children);
13
+ const [presenceState, setPresenceState] = useState(() => isAnimationEnabled && hasContent ? 'open' : undefined);
14
+ const isMounted = isAnimationEnabled && (presenceState === 'open' || presenceState === 'closed');
15
+ const unmountAfterExit = useCallback(() => {
16
+ setPresenceState(undefined);
17
+ setRenderedChildren(undefined);
18
+ }, []);
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 (hasContent) {
30
+ setRenderedChildren(children);
31
+ setPresenceState('open');
32
+ return;
33
+ }
34
+ setPresenceState(current => current === 'open' ? 'closed' : current);
35
+ }, [children, isAnimationEnabled, hasContent]);
36
+ // When `isAnimationEnabled` is false (legacy path), return a no-op result: no mount/unmount, no exit animation.
37
+ // `inline-error.tsx` ignores `presence` and renders the legacy `<span>` + `.empty` instead.
38
+ if (!isAnimationEnabled) {
39
+ return {
40
+ content: children,
41
+ isMounted: false,
42
+ presenceState: undefined,
43
+ onAnimationEnd: animationEndNoop
44
+ };
45
+ }
46
+ return {
47
+ content: renderedChildren,
48
+ isMounted,
49
+ presenceState,
50
+ onAnimationEnd
51
+ };
52
+ }
53
+
54
+ export { useInlineErrorPresence };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@box/blueprint-web",
3
- "version": "15.2.0",
3
+ "version": "15.3.0",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": {
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@ariakit/react": "0.4.21",
49
49
  "@ariakit/react-core": "0.4.21",
50
- "@box/blueprint-web-assets": "^4.121.5",
50
+ "@box/blueprint-web-assets": "^4.121.6",
51
51
  "@internationalized/date": "^3.12.0",
52
52
  "@radix-ui/react-accordion": "1.1.2",
53
53
  "@radix-ui/react-checkbox": "1.0.4",
@@ -77,7 +77,7 @@
77
77
  "type-fest": "^3.2.0"
78
78
  },
79
79
  "devDependencies": {
80
- "@box/storybook-utils": "^0.20.5",
80
+ "@box/storybook-utils": "^0.20.6",
81
81
  "@figma/code-connect": "1.4.4",
82
82
  "@types/react": "^18.0.0",
83
83
  "@types/react-dom": "^18.0.0",