Improve package versioning and cleanup

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-05 20:15:24 +00:00
committed by Patrik Oldsberg
parent 54dd710eb1
commit a47f27c0a9
10 changed files with 59 additions and 36 deletions
@@ -1941,6 +1941,7 @@ export type ToastApi = {
post(toast: ToastMessage): string;
close(key: string): void;
toast$(): Observable<ToastMessageWithKey>;
close$(): Observable<string>;
};
// @public
@@ -111,6 +111,12 @@ export type ToastApi = {
* Observe toasts posted by other parts of the application.
*/
toast$(): Observable<ToastMessageWithKey>;
/**
* Observe close events for programmatic toast dismissal.
* Emits the key of the toast that should be closed.
*/
close$(): Observable<string>;
};
/**
+2 -3
View File
@@ -65,13 +65,12 @@
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.61",
"@react-aria/button": "^3.13.3",
"@react-aria/toast": "^3.0.9",
"@react-hookz/web": "^24.0.0",
"@react-stately/toast": "^3.1.2",
"@remixicon/react": "^4.6.0",
"motion": "^12.0.0",
"react-aria": "^3.41.1",
"react-aria-components": "^1.14.0",
"react-stately": "^3.35.0",
"react-use": "^17.2.4",
"zen-observable": "^0.10.0",
"zod": "^3.25.76"
+14 -1
View File
@@ -88,6 +88,7 @@ export class ToastApiForwarder implements ToastApi {
private readonly subject = new PublishSubject<ToastMessageWithKey>();
private readonly closeSubject = new PublishSubject<string>();
private readonly recentToasts: ToastMessageWithKey[] = [];
private readonly closedKeys = new Set<string>();
private readonly maxBufferSize = 10;
post(toast: ToastMessage): string {
@@ -104,16 +105,28 @@ export class ToastApiForwarder implements ToastApi {
}
close(key: string): void {
// Track closed keys to prevent replaying dismissed toasts
this.closedKeys.add(key);
// Remove from recent buffer if still there
const index = this.recentToasts.findIndex(t => t.key === key);
if (index !== -1) {
this.recentToasts.splice(index, 1);
}
this.closeSubject.next(key);
// Clean up old closed keys when buffer is cleared
if (this.recentToasts.length === 0) {
this.closedKeys.clear();
}
}
toast$(): Observable<ToastMessageWithKey> {
return this.subject.asObservable(this.recentToasts);
// Filter out any toasts that were closed to handle race conditions
const activeToasts = this.recentToasts.filter(
t => !this.closedKeys.has(t.key),
);
return this.subject.asObservable(activeToasts);
}
/**
+20 -8
View File
@@ -24,7 +24,7 @@ import {
useState,
} from 'react';
import { useToast } from '@react-aria/toast';
import { Button } from 'react-aria-components';
import { useButton } from '@react-aria/button';
import { motion } from 'motion/react';
import {
RiInformationLine,
@@ -76,6 +76,9 @@ export const Toast = forwardRef(
toastRef,
);
// Close button ref for useButton hook
const closeButtonRef = useRef<HTMLButtonElement>(null);
// Extract only ARIA and accessibility props from toastProps to avoid
// conflicts with motion.div's event handler types (motion has its own drag API)
const ariaProps = {
@@ -119,6 +122,15 @@ export const Toast = forwardRef(
state.close(toast.key);
};
// Get button props from useButton hook
const { buttonProps } = useButton(
{
'aria-label': closeButtonProps['aria-label'],
onPress: handleClose,
},
closeButtonRef,
);
// Get content from toast
const content = toast.content;
const finalStatus = status || content.status || 'info';
@@ -246,8 +258,8 @@ export const Toast = forwardRef(
)}
{content.links && content.links.length > 0 && (
<div className="toast-links">
{content.links.map((link, idx) => (
<a key={idx} href={link.href}>
{content.links.map(link => (
<a key={link.href} href={link.href}>
{link.label}
</a>
))}
@@ -255,14 +267,14 @@ export const Toast = forwardRef(
)}
</div>
</div>
<Button
slot="close"
{/* eslint-disable-next-line react/forbid-elements */}
<button
{...buttonProps}
ref={closeButtonRef}
className="toast-close-button"
aria-label={closeButtonProps['aria-label']}
onPress={handleClose}
>
<RiCloseLine aria-hidden="true" />
</Button>
</button>
</motion.div>
);
},
@@ -16,7 +16,7 @@
import { forwardRef, Ref, useState, useRef, useCallback, useMemo } from 'react';
import { useToastRegion } from '@react-aria/toast';
import { useToastQueue } from 'react-stately';
import { useToastQueue } from '@react-stately/toast';
import { AnimatePresence } from 'motion/react';
import type { ToastContainerProps } from './types';
import { useInvertedThemeMode } from '../../hooks/useInvertedThemeMode';
@@ -17,7 +17,6 @@
import { useEffect, useRef } from 'react';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { toastApiRef } from '@backstage/frontend-plugin-api';
import { ToastApiForwarder } from '../../apis';
import { ToastContainer } from './ToastContainer';
import { toastQueue } from './ToastQueue';
import type { ToastDisplayProps, ToastContent } from './types';
@@ -103,15 +102,9 @@ export function ToastDisplay(props: ToastDisplayProps) {
return () => subscription.unsubscribe();
}, [toastApi]);
// Subscribe to ToastApi close events
// Subscribe to ToastApi close events for programmatic dismissal
useEffect(() => {
// The ToastApiForwarder exposes a close$() method for programmatic close
const forwarder = toastApi as ToastApiForwarder;
if (!forwarder.close$) {
return undefined;
}
const subscription = forwarder.close$().subscribe(apiKey => {
const subscription = toastApi.close$().subscribe(apiKey => {
const queueKey = toastKeyMap.current.get(apiKey);
if (queueKey) {
toastQueue.close(queueKey);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ToastQueue } from 'react-stately';
import { ToastQueue } from '@react-stately/toast';
import type { ToastContent } from './types';
/**
@@ -55,10 +55,13 @@ export function useInvertedThemeMode(): ThemeMode {
attributeFilter: ['data-theme-mode'],
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ['data-theme-mode'],
});
// Body might not exist in some edge cases (e.g., during SSR or early lifecycle)
if (document.body) {
observer.observe(document.body, {
attributes: true,
attributeFilter: ['data-theme-mode'],
});
}
return () => observer.disconnect();
}, []);
+5 -9
View File
@@ -4301,8 +4301,10 @@ __metadata:
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:^4.0.0-alpha.61"
"@react-aria/button": "npm:^3.13.3"
"@react-aria/toast": "npm:^3.0.9"
"@react-hookz/web": "npm:^24.0.0"
"@react-stately/toast": "npm:^3.1.2"
"@remixicon/react": "npm:^4.6.0"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"
@@ -4311,11 +4313,8 @@ __metadata:
motion: "npm:^12.0.0"
msw: "npm:^1.0.0"
react: "npm:^18.0.2"
react-aria: "npm:^3.41.1"
react-aria-components: "npm:^1.14.0"
react-dom: "npm:^18.0.2"
react-router-dom: "npm:^6.30.2"
react-stately: "npm:^3.35.0"
react-use: "npm:^17.2.4"
zen-observable: "npm:^0.10.0"
zod: "npm:^3.25.76"
@@ -8121,7 +8120,6 @@ __metadata:
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/version-bridge": "workspace:^"
"@react-aria/toast": "npm:^3.0.9"
"@remixicon/react": "npm:^4.6.0"
"@tanstack/react-table": "npm:^8.21.3"
"@types/react": "npm:^18.0.0"
@@ -8130,12 +8128,10 @@ __metadata:
eslint-plugin-storybook: "npm:^10.3.0-alpha.1"
glob: "npm:^11.0.1"
globals: "npm:^15.11.0"
motion: "npm:^12.0.0"
react: "npm:^18.0.2"
react-aria-components: "npm:^1.14.0"
react-dom: "npm:^18.0.2"
react-router-dom: "npm:^6.30.2"
react-stately: "npm:^3.35.0"
storybook: "npm:^10.3.0-alpha.1"
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0
@@ -15568,7 +15564,7 @@ __metadata:
languageName: node
linkType: hard
"@react-aria/button@npm:^3.14.4":
"@react-aria/button@npm:^3.13.3, @react-aria/button@npm:^3.14.4":
version: 3.14.4
resolution: "@react-aria/button@npm:3.14.4"
dependencies:
@@ -44135,7 +44131,7 @@ __metadata:
languageName: node
linkType: hard
"react-aria@npm:^3.41.1, react-aria@npm:^3.45.0":
"react-aria@npm:^3.45.0":
version: 3.46.0
resolution: "react-aria@npm:3.46.0"
dependencies:
@@ -44795,7 +44791,7 @@ __metadata:
languageName: node
linkType: hard
"react-stately@npm:^3.35.0, react-stately@npm:^3.43.0":
"react-stately@npm:^3.43.0":
version: 3.43.0
resolution: "react-stately@npm:3.43.0"
dependencies: