@box/blueprint-web 18.2.1 → 18.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.
- package/dist/lib-esm/blueprint-configuration-context/consts.d.ts +2 -2
- package/dist/lib-esm/blueprint-configuration-context/consts.js +2 -2
- package/dist/lib-esm/components.css +452 -258
- package/dist/lib-esm/grid-list-item/types.d.ts +1 -1
- package/dist/lib-esm/index.css +451 -257
- package/dist/lib-esm/large-list-item/types.d.ts +1 -1
- package/dist/lib-esm/modal/constants.d.ts +1 -0
- package/dist/lib-esm/modal/constants.js +3 -1
- package/dist/lib-esm/modal/index.d.ts +1 -1
- package/dist/lib-esm/modal/modal-header-actions.d.ts +12 -0
- package/dist/lib-esm/modal/modal-header-actions.js +57 -0
- package/dist/lib-esm/modal/modal-header.d.ts +4 -4
- package/dist/lib-esm/modal/modal-header.js +10 -3
- package/dist/lib-esm/modal/modal.d.ts +4 -1
- package/dist/lib-esm/modal/modal.module.js +1 -1
- package/dist/lib-esm/modal/types.d.ts +12 -0
- package/dist/lib-esm/primitives/notification/hooks/use-hover-focus-expand.d.ts +17 -0
- package/dist/lib-esm/primitives/notification/hooks/use-hover-focus-expand.js +85 -0
- package/dist/lib-esm/primitives/notification/hooks/use-notification-exit-animation.d.ts +21 -0
- package/dist/lib-esm/primitives/notification/hooks/use-notification-exit-animation.js +141 -0
- package/dist/lib-esm/primitives/notification/hooks/use-stack-layer-metrics.d.ts +19 -0
- package/dist/lib-esm/primitives/notification/hooks/use-stack-layer-metrics.js +96 -0
- package/dist/lib-esm/primitives/notification/hooks/use-staggered-notification-admission.d.ts +10 -0
- package/dist/lib-esm/primitives/notification/hooks/use-staggered-notification-admission.js +126 -0
- package/dist/lib-esm/primitives/notification/load-animated-notification-viewport.d.ts +11 -0
- package/dist/lib-esm/primitives/notification/load-animated-notification-viewport.js +24 -0
- package/dist/lib-esm/primitives/notification/notification-provider.d.ts +8 -0
- package/dist/lib-esm/primitives/notification/notification-provider.js +29 -3
- package/dist/lib-esm/primitives/notification/notification-stack-utils.d.ts +21 -0
- package/dist/lib-esm/primitives/notification/notification-stack-utils.js +38 -0
- package/dist/lib-esm/primitives/notification/notification-stack.module.js +4 -0
- package/dist/lib-esm/primitives/notification/notification.module.js +1 -1
- package/dist/lib-esm/primitives/notification/notification.viewport-animated.d.ts +7 -0
- package/dist/lib-esm/primitives/notification/notification.viewport-animated.js +147 -0
- package/dist/lib-esm/primitives/notification/notification.viewport.d.ts +5 -0
- package/dist/lib-esm/primitives/notification/notification.viewport.js +51 -1
- package/dist/lib-esm/primitives/notification/private-notification-element-animated.d.ts +32 -0
- package/dist/lib-esm/primitives/notification/private-notification-element-animated.js +198 -0
- package/dist/lib-esm/primitives/notification/private-notification-element.d.ts +3 -0
- package/dist/lib-esm/primitives/notification/private-notification-element.js +3 -0
- package/dist/lib-esm/small-list-item/types.d.ts +1 -1
- package/dist/lib-esm/types/selection-visibility.d.ts +4 -0
- package/dist/lib-esm/util-components/base-grid-list-item/base-grid-list-item-actions.js +4 -2
- package/dist/lib-esm/util-components/base-grid-list-item/base-grid-list-item.module.js +1 -1
- package/dist/lib-esm/util-components/base-grid-list-item/base-grid-list.js +3 -1
- package/dist/lib-esm/util-components/base-grid-list-item/types.d.ts +9 -0
- package/package.json +3 -3
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useRef, useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { prefersReducedMotion, REDUCED_MOTION_QUERY, NOTIFICATION_STACK_STAGGER_MS } from '../notification-stack-utils.js';
|
|
3
|
+
|
|
4
|
+
const idsOf = notifications => notifications ? Array.from(notifications.keys()) : [];
|
|
5
|
+
const clearTimer = timerRef => {
|
|
6
|
+
if (timerRef.current != null) {
|
|
7
|
+
clearTimeout(timerRef.current);
|
|
8
|
+
timerRef.current = null;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const admitAllWaiting = ({
|
|
12
|
+
queueRef,
|
|
13
|
+
admittedIdsRef,
|
|
14
|
+
syncAdmitted
|
|
15
|
+
}) => {
|
|
16
|
+
if (queueRef.current.length === 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const waiting = queueRef.current;
|
|
20
|
+
queueRef.current = [];
|
|
21
|
+
syncAdmitted([...admittedIdsRef.current, ...waiting], waiting[waiting.length - 1] ?? null);
|
|
22
|
+
};
|
|
23
|
+
const pumpQueue = state => {
|
|
24
|
+
const {
|
|
25
|
+
notificationsRef,
|
|
26
|
+
admittedIdsRef,
|
|
27
|
+
queueRef,
|
|
28
|
+
timerRef,
|
|
29
|
+
isAnimationEnabledRef,
|
|
30
|
+
syncAdmitted
|
|
31
|
+
} = state;
|
|
32
|
+
timerRef.current = null;
|
|
33
|
+
if (!isAnimationEnabledRef.current || prefersReducedMotion()) {
|
|
34
|
+
admitAllWaiting(state);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const liveIds = new Set(idsOf(notificationsRef.current));
|
|
38
|
+
while (queueRef.current.length > 0 && !liveIds.has(queueRef.current[0])) {
|
|
39
|
+
queueRef.current.shift();
|
|
40
|
+
}
|
|
41
|
+
const nextId = queueRef.current.shift();
|
|
42
|
+
if (nextId == null) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
syncAdmitted([...admittedIdsRef.current, nextId], nextId);
|
|
46
|
+
if (queueRef.current.length > 0) {
|
|
47
|
+
timerRef.current = setTimeout(() => pumpQueue(state), NOTIFICATION_STACK_STAGGER_MS);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Admits burst arrivals one at a time (`NOTIFICATION_STACK_STAGGER_MS` apart), in the
|
|
52
|
+
* iteration order of `notifications` — pass it oldest-first so the newest lands on top last.
|
|
53
|
+
* First-mount ids, `isAnimationEnabled === false`, and `prefers-reduced-motion` skip the stagger.
|
|
54
|
+
*/
|
|
55
|
+
const useStaggeredNotificationAdmission = (notifications, isAnimationEnabled) => {
|
|
56
|
+
const notificationsRef = useRef(notifications);
|
|
57
|
+
const isAnimationEnabledRef = useRef(isAnimationEnabled);
|
|
58
|
+
notificationsRef.current = notifications;
|
|
59
|
+
isAnimationEnabledRef.current = isAnimationEnabled;
|
|
60
|
+
const [admittedIds, setAdmittedIds] = useState(() => idsOf(notifications));
|
|
61
|
+
const [enteringId, setEnteringId] = useState(null);
|
|
62
|
+
const admittedIdsRef = useRef(admittedIds);
|
|
63
|
+
const queueRef = useRef([]);
|
|
64
|
+
const timerRef = useRef(null);
|
|
65
|
+
const hasHydratedRef = useRef(false);
|
|
66
|
+
const syncAdmitted = (next, nextEnteringId) => {
|
|
67
|
+
admittedIdsRef.current = next;
|
|
68
|
+
setAdmittedIds(next);
|
|
69
|
+
setEnteringId(nextEnteringId);
|
|
70
|
+
};
|
|
71
|
+
const stateRef = useRef({
|
|
72
|
+
notificationsRef,
|
|
73
|
+
admittedIdsRef,
|
|
74
|
+
queueRef,
|
|
75
|
+
timerRef,
|
|
76
|
+
isAnimationEnabledRef,
|
|
77
|
+
syncAdmitted
|
|
78
|
+
});
|
|
79
|
+
stateRef.current.syncAdmitted = syncAdmitted;
|
|
80
|
+
const currentIdsKey = idsOf(notifications).join('|');
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const liveIds = new Set(idsOf(notificationsRef.current));
|
|
83
|
+
admittedIdsRef.current = admittedIdsRef.current.filter(id => liveIds.has(id));
|
|
84
|
+
queueRef.current = queueRef.current.filter(id => liveIds.has(id));
|
|
85
|
+
if (!hasHydratedRef.current) {
|
|
86
|
+
hasHydratedRef.current = true;
|
|
87
|
+
stateRef.current.syncAdmitted([...liveIds], null);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const claimed = new Set([...admittedIdsRef.current, ...queueRef.current]);
|
|
91
|
+
queueRef.current = [...queueRef.current, ...idsOf(notificationsRef.current).filter(id => !claimed.has(id))];
|
|
92
|
+
if (queueRef.current.length === 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!isAnimationEnabledRef.current || prefersReducedMotion()) {
|
|
96
|
+
clearTimer(timerRef);
|
|
97
|
+
admitAllWaiting(stateRef.current);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (timerRef.current == null) {
|
|
101
|
+
pumpQueue(stateRef.current);
|
|
102
|
+
}
|
|
103
|
+
}, [currentIdsKey]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const mediaQuery = window.matchMedia(REDUCED_MOTION_QUERY);
|
|
109
|
+
const handleChange = event => {
|
|
110
|
+
if (event.matches) {
|
|
111
|
+
clearTimer(timerRef);
|
|
112
|
+
admitAllWaiting(stateRef.current);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
116
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
117
|
+
}, [isAnimationEnabled]);
|
|
118
|
+
useEffect(() => () => clearTimer(timerRef), []);
|
|
119
|
+
const admittedSet = useMemo(() => new Set(admittedIds), [admittedIds]);
|
|
120
|
+
return {
|
|
121
|
+
visibleNotifications: notifications ? new Map([...notifications].filter(([id]) => admittedSet.has(id))) : notifications,
|
|
122
|
+
enteringId
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export { useStaggeredNotificationAdmission };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AnimatedNotificationViewport } from './notification.viewport-animated';
|
|
2
|
+
type AnimatedViewportComponent = typeof AnimatedNotificationViewport;
|
|
3
|
+
/**
|
|
4
|
+
* Isolated dynamic import so the sync Notification viewport graph cannot see
|
|
5
|
+
* motion modules. Consumer bundlers emit this as an async chunk — after first
|
|
6
|
+
* paint, not on the critical path.
|
|
7
|
+
*/
|
|
8
|
+
export declare const loadAnimatedNotificationViewport: () => Promise<AnimatedViewportComponent>;
|
|
9
|
+
/** Already-resolved component, so viewports mounted after the first load render it without waiting. */
|
|
10
|
+
export declare const getLoadedAnimatedNotificationViewport: () => AnimatedViewportComponent | null;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
let loaded = null;
|
|
2
|
+
let pending = null;
|
|
3
|
+
/**
|
|
4
|
+
* Isolated dynamic import so the sync Notification viewport graph cannot see
|
|
5
|
+
* motion modules. Consumer bundlers emit this as an async chunk — after first
|
|
6
|
+
* paint, not on the critical path.
|
|
7
|
+
*/
|
|
8
|
+
const loadAnimatedNotificationViewport = () => {
|
|
9
|
+
if (!pending) {
|
|
10
|
+
pending = import(/* webpackChunkName: "bp-notification-stack-motion" */
|
|
11
|
+
'./notification.viewport-animated.js').then(module => {
|
|
12
|
+
loaded = module.AnimatedNotificationViewport;
|
|
13
|
+
return loaded;
|
|
14
|
+
}, error => {
|
|
15
|
+
pending = null;
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return pending;
|
|
20
|
+
};
|
|
21
|
+
/** Already-resolved component, so viewports mounted after the first load render it without waiting. */
|
|
22
|
+
const getLoadedAnimatedNotificationViewport = () => loaded;
|
|
23
|
+
|
|
24
|
+
export { getLoadedAnimatedNotificationViewport, loadAnimatedNotificationViewport };
|
|
@@ -2,6 +2,11 @@ import * as Toast from '@radix-ui/react-toast';
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { type PrivateNotificationElementProps } from './private-notification-element';
|
|
4
4
|
export type NotificationProviderProps = Toast.ToastProviderProps;
|
|
5
|
+
/**
|
|
6
|
+
* Stacked motion cannot coexist with Radix swipe. Preserve the caller/Radix
|
|
7
|
+
* default when Notification animation is off.
|
|
8
|
+
*/
|
|
9
|
+
export declare const resolveNotificationSwipeThreshold: (isAnimationEnabled: boolean, swipeThreshold?: number) => number | undefined;
|
|
5
10
|
interface NotificationContextType {
|
|
6
11
|
/**
|
|
7
12
|
* Creates notification based on passed arguments.
|
|
@@ -23,6 +28,9 @@ interface NotificationContextType {
|
|
|
23
28
|
removeAllNotifications: () => void;
|
|
24
29
|
/**
|
|
25
30
|
* updates notification with given `id`.
|
|
31
|
+
*
|
|
32
|
+
* When Notification animation is enabled, `{ open: false }` removes the notification
|
|
33
|
+
* (a later `{ open: true }` has no effect). Otherwise the entry is kept closed and can be reopened.
|
|
26
34
|
* @param id string
|
|
27
35
|
* @param newProps - new properties of notification
|
|
28
36
|
*/
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import * as Toast from '@radix-ui/react-toast';
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useContext, useState } 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 { deepMerge } from '../../utils/deepMerge.js';
|
|
6
9
|
import { getUniqueId } from '../../utils/useUniqueId.js';
|
|
7
10
|
import { truncateNotificationCopy } from './truncate-notification-copy.js';
|
|
8
11
|
|
|
9
12
|
const DEFAULT_NOTIFICATION_DELAY = 5000;
|
|
13
|
+
/** Unreachable swipe distance — Radix 1.1.5 has no swipe={false}. */
|
|
14
|
+
const DISABLED_SWIPE_THRESHOLD = Number.POSITIVE_INFINITY;
|
|
15
|
+
/**
|
|
16
|
+
* Stacked motion cannot coexist with Radix swipe. Preserve the caller/Radix
|
|
17
|
+
* default when Notification animation is off.
|
|
18
|
+
*/
|
|
19
|
+
const resolveNotificationSwipeThreshold = (isAnimationEnabled, swipeThreshold) => isAnimationEnabled ? DISABLED_SWIPE_THRESHOLD : swipeThreshold;
|
|
10
20
|
const NotificationContext = /*#__PURE__*/React.createContext(null);
|
|
11
21
|
const useNotification = () => {
|
|
12
22
|
const value = useContext(NotificationContext);
|
|
@@ -24,8 +34,13 @@ const useNotification = () => {
|
|
|
24
34
|
const NotificationProvider = props => {
|
|
25
35
|
const {
|
|
26
36
|
children,
|
|
37
|
+
swipeThreshold,
|
|
27
38
|
...rest
|
|
28
39
|
} = props;
|
|
40
|
+
const {
|
|
41
|
+
componentsWithAnimationEnabled
|
|
42
|
+
} = useBlueprintConfiguration();
|
|
43
|
+
const isAnimationEnabled = componentsWithAnimationEnabled.includes('Notification');
|
|
29
44
|
const [notifications, setNotifications] = useState(new Map());
|
|
30
45
|
const addNotification = React.useCallback(args => {
|
|
31
46
|
const {
|
|
@@ -78,6 +93,13 @@ const NotificationProvider = props => {
|
|
|
78
93
|
if (oldNotificationProps === undefined) {
|
|
79
94
|
return oldValue;
|
|
80
95
|
}
|
|
96
|
+
// With stack motion, `open: false` is a dismiss: delete so the closed entry
|
|
97
|
+
// cannot occupy stack depth. The flat path keeps the entry so it can reopen.
|
|
98
|
+
if (isAnimationEnabled && newProps.open === false) {
|
|
99
|
+
const newNotifications = new Map(oldValue);
|
|
100
|
+
newNotifications.delete(id);
|
|
101
|
+
return newNotifications;
|
|
102
|
+
}
|
|
81
103
|
const nextProps = newProps.styledText === undefined ? newProps : {
|
|
82
104
|
...newProps,
|
|
83
105
|
styledText: truncateNotificationCopy(newProps.styledText)
|
|
@@ -86,7 +108,7 @@ const NotificationProvider = props => {
|
|
|
86
108
|
newNotifications.set(id, deepMerge(oldNotificationProps, nextProps));
|
|
87
109
|
return newNotifications;
|
|
88
110
|
});
|
|
89
|
-
}, []);
|
|
111
|
+
}, [isAnimationEnabled]);
|
|
90
112
|
const store = React.useMemo(() => ({
|
|
91
113
|
addNotification,
|
|
92
114
|
notifications,
|
|
@@ -97,6 +119,10 @@ const NotificationProvider = props => {
|
|
|
97
119
|
return jsx(Toast.Provider, {
|
|
98
120
|
...rest,
|
|
99
121
|
duration: DEFAULT_NOTIFICATION_DELAY,
|
|
122
|
+
// Stacked motion cannot coexist with Radix swipe. Leave the default
|
|
123
|
+
// threshold when Notification animation is off so existing consumers
|
|
124
|
+
// keep swipe-to-dismiss. Radix 1.1.5 has no swipe={false}.
|
|
125
|
+
swipeThreshold: resolveNotificationSwipeThreshold(isAnimationEnabled, swipeThreshold),
|
|
100
126
|
children: jsx(NotificationContext.Provider, {
|
|
101
127
|
value: store,
|
|
102
128
|
children: children
|
|
@@ -105,4 +131,4 @@ const NotificationProvider = props => {
|
|
|
105
131
|
};
|
|
106
132
|
NotificationProvider.displayName = 'Notification.Provider';
|
|
107
133
|
|
|
108
|
-
export { NotificationProvider, useNotification };
|
|
134
|
+
export { NotificationProvider, resolveNotificationSwipeThreshold, useNotification };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Collapsed layers shown behind the newest toast before deeper ones are hidden. */
|
|
2
|
+
export declare const NOTIFICATION_STACK_VISIBLE_DEPTH = 3;
|
|
3
|
+
/** Delay between admitting each queued toast during a burst. */
|
|
4
|
+
export declare const NOTIFICATION_STACK_STAGGER_MS = 400;
|
|
5
|
+
/** Token vars for collapsed depth-offset and expanded-layer gap. Read at runtime so layout math cannot drift from CSS. */
|
|
6
|
+
export declare const NOTIFICATION_STACK_DEPTH_OFFSET_VAR = "--bp-space-040";
|
|
7
|
+
export declare const NOTIFICATION_STACK_GAP_VAR = "--bp-space-020";
|
|
8
|
+
export declare const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
9
|
+
export declare const prefersReducedMotion: () => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Radix does not mount Toast.Root when `open === false`. Closed store entries
|
|
12
|
+
* must not occupy stack depth or contribute to height measurements.
|
|
13
|
+
*/
|
|
14
|
+
export declare const isOpenNotification: (notification: {
|
|
15
|
+
open?: boolean;
|
|
16
|
+
}) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves a `:root` custom property to px. `parseFloat` alone treats rem as px
|
|
19
|
+
* (`0.5rem` → `0.5px`) and collapses the expanded-stack gap.
|
|
20
|
+
*/
|
|
21
|
+
export declare const readRootPixelVar: (cssVar: string) => number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Collapsed layers shown behind the newest toast before deeper ones are hidden. */
|
|
2
|
+
const NOTIFICATION_STACK_VISIBLE_DEPTH = 3;
|
|
3
|
+
/** Delay between admitting each queued toast during a burst. */
|
|
4
|
+
const NOTIFICATION_STACK_STAGGER_MS = 400;
|
|
5
|
+
/** Token vars for collapsed depth-offset and expanded-layer gap. Read at runtime so layout math cannot drift from CSS. */
|
|
6
|
+
const NOTIFICATION_STACK_DEPTH_OFFSET_VAR = '--bp-space-040';
|
|
7
|
+
const NOTIFICATION_STACK_GAP_VAR = '--bp-space-020';
|
|
8
|
+
const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
|
9
|
+
const prefersReducedMotion = () => typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia(REDUCED_MOTION_QUERY).matches;
|
|
10
|
+
/**
|
|
11
|
+
* Radix does not mount Toast.Root when `open === false`. Closed store entries
|
|
12
|
+
* must not occupy stack depth or contribute to height measurements.
|
|
13
|
+
*/
|
|
14
|
+
const isOpenNotification = notification => notification.open !== false;
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a `:root` custom property to px. `parseFloat` alone treats rem as px
|
|
17
|
+
* (`0.5rem` → `0.5px`) and collapses the expanded-stack gap.
|
|
18
|
+
*/
|
|
19
|
+
const readRootPixelVar = cssVar => {
|
|
20
|
+
if (typeof document === 'undefined') {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
const raw = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
|
|
24
|
+
if (!raw) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const value = Number.parseFloat(raw);
|
|
28
|
+
if (Number.isNaN(value)) {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (raw.endsWith('rem')) {
|
|
32
|
+
const rootFontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
33
|
+
return value * (Number.isNaN(rootFontSize) ? 16 : rootFontSize);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { NOTIFICATION_STACK_DEPTH_OFFSET_VAR, NOTIFICATION_STACK_GAP_VAR, NOTIFICATION_STACK_STAGGER_MS, NOTIFICATION_STACK_VISIBLE_DEPTH, REDUCED_MOTION_QUERY, isOpenNotification, prefersReducedMotion, readRootPixelVar };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import '../../index.css';
|
|
2
|
-
var styles = {"viewport":"bp_notification_module_viewport--
|
|
2
|
+
var styles = {"viewport":"bp_notification_module_viewport--cc8a5","root":"bp_notification_module_root--cc8a5","container":"bp_notification_module_container--cc8a5","messageRow":"bp_notification_module_messageRow--cc8a5","withButtons":"bp_notification_module_withButtons--cc8a5","info":"bp_notification_module_info--cc8a5","success":"bp_notification_module_success--cc8a5","warning":"bp_notification_module_warning--cc8a5","error":"bp_notification_module_error--cc8a5","typeIconContainer":"bp_notification_module_typeIconContainer--cc8a5","typeIcon":"bp_notification_module_typeIcon--cc8a5","text":"bp_notification_module_text--cc8a5","contentButtonSection":"bp_notification_module_contentButtonSection--cc8a5","contentButtons":"bp_notification_module_contentButtons--cc8a5","closeButtonSection":"bp_notification_module_closeButtonSection--cc8a5","closeButton":"bp_notification_module_closeButton--cc8a5"};
|
|
3
3
|
|
|
4
4
|
export { styles as default };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ToastViewportProps } from '@radix-ui/react-toast';
|
|
2
|
+
export type AnimatedNotificationViewportProps = ToastViewportProps;
|
|
3
|
+
/**
|
|
4
|
+
* Stacked-deck viewport. Imported dynamically after first paint so this module
|
|
5
|
+
* (hooks + stack CSS + animated toast) is not on the critical path.
|
|
6
|
+
*/
|
|
7
|
+
export declare const AnimatedNotificationViewport: import("react").ForwardRefExoticComponent<ToastViewportProps & import("react").RefAttributes<HTMLOListElement>>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { Viewport } from '@radix-ui/react-toast';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { forwardRef, useState, useCallback, useEffect } from 'react';
|
|
5
|
+
import { composeEventHandlers } from '../../utils/composeEventHandlers.js';
|
|
6
|
+
import { useHoverFocusExpand } from './hooks/use-hover-focus-expand.js';
|
|
7
|
+
import { useStackLayerMetrics } from './hooks/use-stack-layer-metrics.js';
|
|
8
|
+
import { useStaggeredNotificationAdmission } from './hooks/use-staggered-notification-admission.js';
|
|
9
|
+
import { useNotification } from './notification-provider.js';
|
|
10
|
+
import { isOpenNotification, readRootPixelVar, NOTIFICATION_STACK_VISIBLE_DEPTH, NOTIFICATION_STACK_GAP_VAR, NOTIFICATION_STACK_DEPTH_OFFSET_VAR } from './notification-stack-utils.js';
|
|
11
|
+
import stackStyles from './notification-stack.module.js';
|
|
12
|
+
import styles from './notification.module.js';
|
|
13
|
+
import { AnimatedPrivateNotificationElement } from './private-notification-element-animated.js';
|
|
14
|
+
|
|
15
|
+
const VIEWPORT_DEFAULT_HOTKEY = ['F8'];
|
|
16
|
+
/** Stack expand/collapse must stay consistent even if a consumer handler calls `preventDefault()`. */
|
|
17
|
+
const COMPOSE_OPTIONS = {
|
|
18
|
+
checkForDefaultPrevented: false
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Stacked-deck viewport. Imported dynamically after first paint so this module
|
|
22
|
+
* (hooks + stack CSS + animated toast) is not on the critical path.
|
|
23
|
+
*/
|
|
24
|
+
const AnimatedNotificationViewport = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
25
|
+
const {
|
|
26
|
+
className,
|
|
27
|
+
hotkey = VIEWPORT_DEFAULT_HOTKEY,
|
|
28
|
+
style,
|
|
29
|
+
onBlur,
|
|
30
|
+
onFocus,
|
|
31
|
+
onMouseEnter,
|
|
32
|
+
onMouseLeave,
|
|
33
|
+
onPointerLeave,
|
|
34
|
+
...rest
|
|
35
|
+
} = props;
|
|
36
|
+
const {
|
|
37
|
+
notifications
|
|
38
|
+
} = useNotification();
|
|
39
|
+
// Admission and render order stay oldest-first (arrival order): toasts portal into the
|
|
40
|
+
// viewport one by one, and Radix routes Escape to the last-mounted toast and Tab in reverse
|
|
41
|
+
// DOM order, so the newest must mount last. Depth (0 = front) is derived newest-first below;
|
|
42
|
+
// the visual stacking is CSS-only. Closed (`open === false`) entries stay out of admission
|
|
43
|
+
// and metrics — Radix does not render their toast root. Exiting toasts stay mounted for
|
|
44
|
+
// motion but drop out of stack metrics immediately (popLayout).
|
|
45
|
+
const arrivalEntries = Array.from(notifications.entries()).filter(([, notification]) => isOpenNotification(notification));
|
|
46
|
+
const {
|
|
47
|
+
visibleNotifications,
|
|
48
|
+
enteringId
|
|
49
|
+
} = useStaggeredNotificationAdmission(new Map(arrivalEntries), true);
|
|
50
|
+
const visibleEntries = Array.from(visibleNotifications?.entries() ?? arrivalEntries);
|
|
51
|
+
const [exitingIds, setExitingIds] = useState(() => new Set());
|
|
52
|
+
const handleExitStart = useCallback(id => {
|
|
53
|
+
setExitingIds(previous => {
|
|
54
|
+
if (previous.has(id)) {
|
|
55
|
+
return previous;
|
|
56
|
+
}
|
|
57
|
+
const next = new Set(previous);
|
|
58
|
+
next.add(id);
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
}, []);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const liveIds = new Set(notifications.keys());
|
|
64
|
+
setExitingIds(previous => {
|
|
65
|
+
if (previous.size === 0) {
|
|
66
|
+
return previous;
|
|
67
|
+
}
|
|
68
|
+
let changed = false;
|
|
69
|
+
const next = new Set();
|
|
70
|
+
previous.forEach(id => {
|
|
71
|
+
if (liveIds.has(id)) {
|
|
72
|
+
next.add(id);
|
|
73
|
+
} else {
|
|
74
|
+
changed = true;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return changed ? next : previous;
|
|
78
|
+
});
|
|
79
|
+
}, [notifications]);
|
|
80
|
+
const layoutEntries = visibleEntries.filter(([id, notification]) => !exitingIds.has(id) && isOpenNotification(notification));
|
|
81
|
+
const layoutCount = layoutEntries.length;
|
|
82
|
+
const depthById = new Map(layoutEntries.map(([id], index) => [id, layoutCount - 1 - index]));
|
|
83
|
+
const {
|
|
84
|
+
metrics,
|
|
85
|
+
getLayerRef
|
|
86
|
+
} = useStackLayerMetrics(layoutCount, readRootPixelVar(NOTIFICATION_STACK_DEPTH_OFFSET_VAR), readRootPixelVar(NOTIFICATION_STACK_GAP_VAR));
|
|
87
|
+
const {
|
|
88
|
+
isExpanded,
|
|
89
|
+
stackRef,
|
|
90
|
+
handleMouseEnter,
|
|
91
|
+
handleMouseLeave,
|
|
92
|
+
handlePointerLeave,
|
|
93
|
+
handleFocus,
|
|
94
|
+
handleBlur
|
|
95
|
+
} = useHoverFocusExpand(layoutCount > 1, true);
|
|
96
|
+
const stackHeight = isExpanded ? metrics.expandedHeight : metrics.collapsedHeight;
|
|
97
|
+
const stackStyle = {
|
|
98
|
+
...style,
|
|
99
|
+
'--notification-stack-height': `${stackHeight}px`,
|
|
100
|
+
'--notification-stack-visible-depth': NOTIFICATION_STACK_VISIBLE_DEPTH
|
|
101
|
+
};
|
|
102
|
+
// Radix portals each Toast.Root <li> straight into this viewport, so the viewport
|
|
103
|
+
// itself is the stack and each toast carries its own layer styles — no wrapper div.
|
|
104
|
+
return jsx(Viewport, {
|
|
105
|
+
...rest,
|
|
106
|
+
ref: node => {
|
|
107
|
+
stackRef.current = node;
|
|
108
|
+
if (typeof forwardedRef === 'function') {
|
|
109
|
+
forwardedRef(node);
|
|
110
|
+
} else if (forwardedRef) {
|
|
111
|
+
forwardedRef.current = node;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
className: clsx(styles.viewport, stackStyles.stack, {
|
|
115
|
+
[stackStyles.isExpanded]: isExpanded
|
|
116
|
+
}, className),
|
|
117
|
+
"data-bp-animated": "true",
|
|
118
|
+
hotkey: hotkey,
|
|
119
|
+
onBlur: composeEventHandlers(onBlur, handleBlur, COMPOSE_OPTIONS),
|
|
120
|
+
onFocus: composeEventHandlers(onFocus, handleFocus, COMPOSE_OPTIONS),
|
|
121
|
+
onMouseEnter: composeEventHandlers(onMouseEnter, handleMouseEnter, COMPOSE_OPTIONS),
|
|
122
|
+
onMouseLeave: composeEventHandlers(onMouseLeave, handleMouseLeave, COMPOSE_OPTIONS),
|
|
123
|
+
onPointerLeave: composeEventHandlers(onPointerLeave, handlePointerLeave, COMPOSE_OPTIONS),
|
|
124
|
+
style: stackStyle,
|
|
125
|
+
children: visibleEntries.map(([id, {
|
|
126
|
+
key,
|
|
127
|
+
...notificationProps
|
|
128
|
+
}]) => {
|
|
129
|
+
const depth = depthById.get(id);
|
|
130
|
+
const inLayout = depth !== undefined;
|
|
131
|
+
return jsx(AnimatedPrivateNotificationElement, {
|
|
132
|
+
...notificationProps,
|
|
133
|
+
depth: inLayout ? depth : 0,
|
|
134
|
+
expandOffset: inLayout ? metrics.offsets[depth] ?? 0 : 0,
|
|
135
|
+
isAnimationEnabled: true,
|
|
136
|
+
isBeyondVisibleDepth: inLayout && depth >= NOTIFICATION_STACK_VISIBLE_DEPTH,
|
|
137
|
+
isEntering: id === enteringId,
|
|
138
|
+
layerRef: inLayout ? getLayerRef(depth) : undefined,
|
|
139
|
+
onExitStart: handleExitStart,
|
|
140
|
+
stackId: id
|
|
141
|
+
}, id);
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
AnimatedNotificationViewport.displayName = 'Notification.Viewport.Animated';
|
|
146
|
+
|
|
147
|
+
export { AnimatedNotificationViewport };
|
|
@@ -4,5 +4,10 @@ export type NotificationViewportProps = ToastViewportProps;
|
|
|
4
4
|
* Renders container with Notifications.
|
|
5
5
|
*
|
|
6
6
|
* Based on Radix-UI [Viewport](https://www.radix-ui.com/docs/primitives/components/toast#viewport).
|
|
7
|
+
*
|
|
8
|
+
* Stacked-deck motion is loaded after first paint when Notification animation
|
|
9
|
+
* is on, so the extra hooks/CSS are not on the critical path. Toasts are held
|
|
10
|
+
* (nothing renders) until it resolves: swapping from the flat viewport would
|
|
11
|
+
* remount visible toasts, restarting their timers and re-announcing them.
|
|
7
12
|
*/
|
|
8
13
|
export declare const NotificationViewport: import("react").ForwardRefExoticComponent<ToastViewportProps & import("react").RefAttributes<HTMLOListElement>>;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { Viewport } from '@radix-ui/react-toast';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import { forwardRef, useEffect } from 'react';
|
|
4
|
+
import { forwardRef, useState, useEffect } 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 { isProduction } from '../../utils/isProduction.js';
|
|
9
|
+
import { getLoadedAnimatedNotificationViewport, loadAnimatedNotificationViewport } from './load-animated-notification-viewport.js';
|
|
5
10
|
import { useNotification } from './notification-provider.js';
|
|
6
11
|
import styles from './notification.module.js';
|
|
7
12
|
import { PrivateNotificationElement } from './private-notification-element.js';
|
|
@@ -11,6 +16,11 @@ const VIEWPORT_DEFAULT_HOTKEY = ['F8'];
|
|
|
11
16
|
* Renders container with Notifications.
|
|
12
17
|
*
|
|
13
18
|
* Based on Radix-UI [Viewport](https://www.radix-ui.com/docs/primitives/components/toast#viewport).
|
|
19
|
+
*
|
|
20
|
+
* Stacked-deck motion is loaded after first paint when Notification animation
|
|
21
|
+
* is on, so the extra hooks/CSS are not on the critical path. Toasts are held
|
|
22
|
+
* (nothing renders) until it resolves: swapping from the flat viewport would
|
|
23
|
+
* remount visible toasts, restarting their timers and re-announcing them.
|
|
14
24
|
*/
|
|
15
25
|
const NotificationViewport = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
16
26
|
const {
|
|
@@ -21,6 +31,12 @@ const NotificationViewport = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
21
31
|
const {
|
|
22
32
|
notifications
|
|
23
33
|
} = useNotification();
|
|
34
|
+
const {
|
|
35
|
+
componentsWithAnimationEnabled
|
|
36
|
+
} = useBlueprintConfiguration();
|
|
37
|
+
const isAnimationEnabled = componentsWithAnimationEnabled.includes('Notification');
|
|
38
|
+
const [AnimatedViewport, setAnimatedViewport] = useState(() => isAnimationEnabled ? getLoadedAnimatedNotificationViewport() : null);
|
|
39
|
+
const [hasLoadFailed, setHasLoadFailed] = useState(false);
|
|
24
40
|
useEffect(() => {
|
|
25
41
|
const handleKeyDown = event => {
|
|
26
42
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -37,6 +53,39 @@ const NotificationViewport = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
37
53
|
});
|
|
38
54
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
39
55
|
}, [hotkey]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!isAnimationEnabled) {
|
|
58
|
+
setAnimatedViewport(null);
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
setHasLoadFailed(false);
|
|
63
|
+
loadAnimatedNotificationViewport().then(component => {
|
|
64
|
+
if (!cancelled) {
|
|
65
|
+
setAnimatedViewport(() => component);
|
|
66
|
+
}
|
|
67
|
+
}).catch(() => {
|
|
68
|
+
if (!cancelled) {
|
|
69
|
+
setHasLoadFailed(true);
|
|
70
|
+
}
|
|
71
|
+
if (!isProduction()) {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.warn('Failed to load animated notification viewport');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return () => {
|
|
77
|
+
cancelled = true;
|
|
78
|
+
};
|
|
79
|
+
}, [isAnimationEnabled]);
|
|
80
|
+
if (isAnimationEnabled && AnimatedViewport) {
|
|
81
|
+
return jsx(AnimatedViewport, {
|
|
82
|
+
...props,
|
|
83
|
+
ref: forwardedRef
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (isAnimationEnabled && !hasLoadFailed) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
40
89
|
return jsx(Viewport, {
|
|
41
90
|
...rest,
|
|
42
91
|
ref: forwardedRef,
|
|
@@ -50,5 +99,6 @@ const NotificationViewport = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
50
99
|
}, key))
|
|
51
100
|
});
|
|
52
101
|
});
|
|
102
|
+
NotificationViewport.displayName = 'Notification.Viewport';
|
|
53
103
|
|
|
54
104
|
export { NotificationViewport };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type PrivateNotificationElementProps } from './private-notification-element';
|
|
2
|
+
/**
|
|
3
|
+
* Internal-only props supplied by the animated viewport, never by app code calling
|
|
4
|
+
* `addNotification` — not part of the public `Notification` API surface.
|
|
5
|
+
*/
|
|
6
|
+
export interface PrivateNotificationElementInternalProps extends PrivateNotificationElementProps {
|
|
7
|
+
/** Whether the stacked-deck animation feature (`data-bp-animated`) is on for this render. */
|
|
8
|
+
isAnimationEnabled?: boolean;
|
|
9
|
+
/** True only for the single toast currently being staggered in; drives the enter animation. */
|
|
10
|
+
isEntering?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Position in the deck, newest first. Radix portals this toast's `<li>` directly into the
|
|
13
|
+
* viewport, so the toast has to carry its own layer styling rather than being wrapped in a
|
|
14
|
+
* positioned container by `NotificationViewport`.
|
|
15
|
+
*/
|
|
16
|
+
depth?: number;
|
|
17
|
+
/** Translate distance for this layer when the deck is expanded, in px. */
|
|
18
|
+
expandOffset?: number;
|
|
19
|
+
/** True for layers stacked deeper than the visible peek depth; they're hidden while collapsed. */
|
|
20
|
+
isBeyondVisibleDepth?: boolean;
|
|
21
|
+
/** Lets the viewport measure this layer's rendered height for the deck's height math. */
|
|
22
|
+
layerRef?: (node: HTMLLIElement | null) => void;
|
|
23
|
+
/** Provider map key; paired with `onExitStart` so the viewport can pop this layer out of layout. */
|
|
24
|
+
stackId?: string;
|
|
25
|
+
/** Fires on the first exit frame so siblings can reflow in parallel with the exit motion. */
|
|
26
|
+
onExitStart?: (id: string) => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stacked-deck toast. Loaded only from the animated viewport chunk so the flat
|
|
30
|
+
* Notification path stays off the first-paint graph.
|
|
31
|
+
*/
|
|
32
|
+
export declare function AnimatedPrivateNotificationElement(props: PrivateNotificationElementInternalProps): import("react/jsx-runtime").JSX.Element;
|