Finalise animation and cleaning report
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
committed by
Patrik Oldsberg
parent
183cd70978
commit
79a61abc5a
@@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ToastRegion, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
import { ToastContainer, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
import type { ToastContent } from '@backstage/ui';
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
@@ -25,7 +25,7 @@ export function Default() {
|
||||
export function StatusVariants() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -79,7 +79,7 @@ export function StatusVariants() {
|
||||
export function WithDescription() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
@@ -98,7 +98,7 @@ export function WithDescription() {
|
||||
export function WithoutDescription() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -128,7 +128,7 @@ export function WithoutDescription() {
|
||||
export function AutoDismiss() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -168,7 +168,7 @@ export function ProgrammaticControl() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (!toastKey) {
|
||||
@@ -197,7 +197,7 @@ export function ProgrammaticControl() {
|
||||
export function QueueManagement() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export const toastUsageSnippet = `import { ToastRegion, toastQueue } from '@backstage/ui';
|
||||
export const toastUsageSnippet = `import { ToastContainer, toastQueue } from '@backstage/ui';
|
||||
|
||||
// Place ToastRegion once in your app root
|
||||
// Place ToastContainer once in your app root
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<YourAppContent />
|
||||
</>
|
||||
);
|
||||
@@ -13,12 +13,12 @@ function App() {
|
||||
// Trigger toasts from anywhere
|
||||
toastQueue.add({ title: 'Success!', status: 'success' });`;
|
||||
|
||||
export const defaultSnippet = `import { ToastRegion, toastQueue, Button } from '@backstage/ui';
|
||||
export const defaultSnippet = `import { ToastContainer, toastQueue, Button } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
@@ -33,12 +33,12 @@ export function Example() {
|
||||
);
|
||||
}`;
|
||||
|
||||
export const statusVariantsSnippet = `import { ToastRegion, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
export const statusVariantsSnippet = `import { ToastContainer, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -89,12 +89,12 @@ export function Example() {
|
||||
);
|
||||
}`;
|
||||
|
||||
export const withDescriptionSnippet = `import { ToastRegion, toastQueue, Button } from '@backstage/ui';
|
||||
export const withDescriptionSnippet = `import { ToastContainer, toastQueue, Button } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
@@ -110,12 +110,12 @@ export function Example() {
|
||||
);
|
||||
}`;
|
||||
|
||||
export const withoutDescriptionSnippet = `import { ToastRegion, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
export const withoutDescriptionSnippet = `import { ToastContainer, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -142,12 +142,12 @@ export function Example() {
|
||||
);
|
||||
}`;
|
||||
|
||||
export const autoDismissSnippet = `import { ToastRegion, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
export const autoDismissSnippet = `import { ToastContainer, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -183,14 +183,14 @@ export function Example() {
|
||||
}`;
|
||||
|
||||
export const programmaticControlSnippet = `import { useState } from 'react';
|
||||
import { ToastRegion, toastQueue, Button } from '@backstage/ui';
|
||||
import { ToastContainer, toastQueue, Button } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
const [toastKey, setToastKey] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (!toastKey) {
|
||||
@@ -216,12 +216,12 @@ export function Example() {
|
||||
);
|
||||
}`;
|
||||
|
||||
export const queueManagementSnippet = `import { ToastRegion, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
export const queueManagementSnippet = `import { ToastContainer, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() => {
|
||||
|
||||
+25
-82
@@ -31,7 +31,6 @@ import type { MenuSectionProps as MenuSectionProps_2 } from 'react-aria-componen
|
||||
import type { MenuTriggerProps as MenuTriggerProps_2 } from 'react-aria-components';
|
||||
import type { ModalOverlayProps } from 'react-aria-components';
|
||||
import { PopoverProps as PopoverProps_2 } from 'react-aria-components';
|
||||
import type { QueuedToast } from 'react-stately';
|
||||
import type { RadioGroupProps as RadioGroupProps_2 } from 'react-aria-components';
|
||||
import type { RadioProps as RadioProps_2 } from 'react-aria-components';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -56,7 +55,6 @@ import type { TagListProps } from 'react-aria-components';
|
||||
import type { TagProps as TagProps_2 } from 'react-aria-components';
|
||||
import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components';
|
||||
import { ToastQueue } from 'react-stately';
|
||||
import type { ToastState } from 'react-stately';
|
||||
import type { ToggleButtonGroupProps as ToggleButtonGroupProps_2 } from 'react-aria-components';
|
||||
import type { ToggleButtonProps as ToggleButtonProps_2 } from 'react-aria-components';
|
||||
import { TooltipProps as TooltipProps_2 } from 'react-aria-components';
|
||||
@@ -2183,10 +2181,33 @@ export type TextVariants =
|
||||
export type TextWeights = 'regular' | 'bold';
|
||||
|
||||
// @public
|
||||
export const Toast: ForwardRefExoticComponent<
|
||||
ToastProps & RefAttributes<HTMLDivElement>
|
||||
export const ToastContainer: ForwardRefExoticComponent<
|
||||
ToastContainerProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const ToastContainerDefinition: {
|
||||
readonly styles: {
|
||||
readonly [key: string]: string;
|
||||
};
|
||||
readonly classNames: {
|
||||
readonly container: 'bui-ToastContainer';
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly queue: {};
|
||||
readonly className: {};
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ToastContainerOwnProps = {
|
||||
queue: ToastQueue<ToastContent>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ToastContainerProps extends ToastContainerOwnProps {}
|
||||
|
||||
// @public
|
||||
export interface ToastContent {
|
||||
description?: ReactNode;
|
||||
@@ -2195,87 +2216,9 @@ export interface ToastContent {
|
||||
title: ReactNode;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const ToastDefinition: {
|
||||
readonly styles: {
|
||||
readonly [key: string]: string;
|
||||
};
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-Toast';
|
||||
readonly content: 'bui-ToastContent';
|
||||
readonly title: 'bui-ToastTitle';
|
||||
readonly description: 'bui-ToastDescription';
|
||||
readonly icon: 'bui-ToastIcon';
|
||||
readonly closeButton: 'bui-ToastCloseButton';
|
||||
};
|
||||
readonly surface: 'container';
|
||||
readonly propDefs: {
|
||||
readonly toast: {};
|
||||
readonly state: {};
|
||||
readonly index: {};
|
||||
readonly isExpanded: {};
|
||||
readonly onClose: {};
|
||||
readonly status: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly icon: {};
|
||||
readonly surface: {};
|
||||
readonly className: {};
|
||||
readonly style: {};
|
||||
readonly expandedY: {};
|
||||
readonly collapsedHeight: {};
|
||||
readonly onHeightChange: {};
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ToastOwnProps = ContainerSurfaceProps & {
|
||||
toast: QueuedToast<ToastContent>;
|
||||
state: ToastState<ToastContent>;
|
||||
index?: number;
|
||||
isExpanded?: boolean;
|
||||
onClose?: () => void;
|
||||
status?: Responsive<'info' | 'success' | 'warning' | 'danger'>;
|
||||
icon?: boolean | ReactElement;
|
||||
expandedY?: number;
|
||||
collapsedHeight?: number;
|
||||
onHeightChange?: (key: string, height: number) => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ToastProps extends ToastOwnProps {}
|
||||
|
||||
// @public
|
||||
export const toastQueue: ToastQueue<ToastContent>;
|
||||
|
||||
// @public
|
||||
export const ToastRegion: ForwardRefExoticComponent<
|
||||
ToastRegionProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const ToastRegionDefinition: {
|
||||
readonly styles: {
|
||||
readonly [key: string]: string;
|
||||
};
|
||||
readonly classNames: {
|
||||
readonly region: 'bui-ToastRegion';
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly queue: {};
|
||||
readonly className: {};
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ToastRegionOwnProps = {
|
||||
queue: ToastQueue<ToastContent>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ToastRegionProps extends ToastRegionOwnProps {}
|
||||
|
||||
// @public (undocumented)
|
||||
export const ToggleButton: ForwardRefExoticComponent<
|
||||
ToggleButtonProps & RefAttributes<HTMLButtonElement>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
@layer tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
/* Toast Region - Container for all toasts (bottom center) */
|
||||
.bui-ToastRegion {
|
||||
/* Toast Container - Container for all toasts (bottom center) */
|
||||
.bui-ToastContainer {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
left: 50%;
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.bui-ToastRegion {
|
||||
.bui-ToastContainer {
|
||||
width: 350px;
|
||||
bottom: 2rem;
|
||||
}
|
||||
@@ -51,6 +51,7 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: var(--bui-space-4);
|
||||
gap: var(--bui-space-3);
|
||||
|
||||
/* Appearance */
|
||||
border-radius: var(--bui-radius-3);
|
||||
@@ -111,8 +112,7 @@
|
||||
color: var(--bui-fg-danger);
|
||||
}
|
||||
|
||||
/* Toast Content */
|
||||
.bui-ToastContent {
|
||||
.bui-ToastWrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--bui-space-3);
|
||||
@@ -120,6 +120,12 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bui-ToastContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--bui-space-1);
|
||||
}
|
||||
|
||||
/* Icon */
|
||||
.bui-ToastIcon {
|
||||
flex-shrink: 0;
|
||||
@@ -139,6 +145,7 @@
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
font-size: var(--bui-font-size-3);
|
||||
word-wrap: break-word;
|
||||
margin-top: var(--bui-space-0_5);
|
||||
}
|
||||
|
||||
/* Description */
|
||||
@@ -146,7 +153,6 @@
|
||||
color: var(--bui-fg-secondary);
|
||||
font-size: var(--bui-font-size-3);
|
||||
opacity: 0.9;
|
||||
margin-top: var(--bui-space-1);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import preview from '../../../../../.storybook/preview';
|
||||
import { ToastRegion, toastQueue } from './index';
|
||||
import { ToastContainer, toastQueue } from './index';
|
||||
import { Flex } from '../Flex';
|
||||
import { Button } from '../Button';
|
||||
|
||||
const meta = preview.meta({
|
||||
title: 'Backstage UI/Toast',
|
||||
component: ToastRegion,
|
||||
component: ToastContainer,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
@@ -109,16 +109,25 @@ const randomToasts = [
|
||||
export const Default = meta.story({
|
||||
render: () => (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Button
|
||||
onPress={() => {
|
||||
const toast =
|
||||
randomToasts[Math.floor(Math.random() * randomToasts.length)];
|
||||
toastQueue.add(toast);
|
||||
}}
|
||||
>
|
||||
Show Toast
|
||||
</Button>
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() => {
|
||||
const toast =
|
||||
randomToasts[Math.floor(Math.random() * randomToasts.length)];
|
||||
toastQueue.add(toast);
|
||||
}}
|
||||
>
|
||||
Add Random Toast
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
toastQueue.clear();
|
||||
}}
|
||||
>
|
||||
Clear All Toasts
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
),
|
||||
});
|
||||
@@ -126,7 +135,7 @@ export const Default = meta.story({
|
||||
export const StatusVariants = meta.story({
|
||||
render: () => (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -180,7 +189,7 @@ export const StatusVariants = meta.story({
|
||||
export const WithoutDescription = meta.story({
|
||||
render: () => (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -210,7 +219,7 @@ export const WithoutDescription = meta.story({
|
||||
export const WithoutIcons = meta.story({
|
||||
render: () => (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -242,7 +251,7 @@ export const WithoutIcons = meta.story({
|
||||
export const AutoDismiss = meta.story({
|
||||
render: () => (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
@@ -297,7 +306,7 @@ export const ProgrammaticDismiss = meta.story({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (!toastKey) {
|
||||
@@ -327,7 +336,7 @@ export const ProgrammaticDismiss = meta.story({
|
||||
export const QueueManagement = meta.story({
|
||||
render: () => (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<ToastContainer queue={toastQueue} />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
isValidElement,
|
||||
ReactElement,
|
||||
useRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useToast } from '@react-aria/toast';
|
||||
@@ -45,7 +45,7 @@ const manuallyClosingToasts = new Set<string>();
|
||||
* A Toast displays a brief, temporary notification of actions, errors, or other events in an application.
|
||||
*
|
||||
* @remarks
|
||||
* The Toast component is typically used within a ToastRegion and managed by a ToastQueue.
|
||||
* The Toast component is used internally by ToastContainer and managed by a ToastQueue.
|
||||
* It supports multiple status variants (info, success, warning, danger) and can display
|
||||
* a title, description, and optional icon. Toasts can be dismissed manually or automatically.
|
||||
*
|
||||
@@ -70,7 +70,7 @@ const manuallyClosingToasts = new Set<string>();
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
export const Toast = forwardRef(
|
||||
(props: ToastProps, ref: Ref<HTMLDivElement>) => {
|
||||
@@ -89,6 +89,7 @@ export const Toast = forwardRef(
|
||||
icon,
|
||||
expandedY: expandedYProp = 0,
|
||||
collapsedHeight,
|
||||
naturalHeight,
|
||||
onHeightChange,
|
||||
} = ownProps;
|
||||
|
||||
@@ -117,24 +118,26 @@ export const Toast = forwardRef(
|
||||
|
||||
// Track whether we've measured this toast's natural height
|
||||
const [hasMeasured, setHasMeasured] = useState(false);
|
||||
// Store the measured natural height locally to avoid re-measurement issues
|
||||
const naturalHeightRef = useRef<number | null>(null);
|
||||
|
||||
// Measure this toast's natural height on mount (before constraints are applied)
|
||||
useEffect(() => {
|
||||
// Measure this toast's natural height on mount (before paint)
|
||||
// Using useLayoutEffect ensures we measure before the browser paints
|
||||
useLayoutEffect(() => {
|
||||
if (!onHeightChange) return;
|
||||
if (hasMeasured) return; // Only measure once
|
||||
if (naturalHeightRef.current) return; // Already measured
|
||||
|
||||
const element = toastRef.current;
|
||||
if (!element) return;
|
||||
|
||||
// Measure on next frame (after initial render but before constraint is visible)
|
||||
requestAnimationFrame(() => {
|
||||
const height = element.offsetHeight;
|
||||
if (height > 0) {
|
||||
onHeightChange(toast.key, height);
|
||||
setHasMeasured(true);
|
||||
}
|
||||
});
|
||||
}, [toast.key, onHeightChange, hasMeasured]);
|
||||
// Measure immediately - useLayoutEffect runs before paint
|
||||
const height = element.getBoundingClientRect().height;
|
||||
if (height > 0) {
|
||||
naturalHeightRef.current = height;
|
||||
onHeightChange(toast.key, height);
|
||||
setHasMeasured(true);
|
||||
}
|
||||
}, [toast.key, onHeightChange]);
|
||||
|
||||
// Close button ref and props
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -215,11 +218,35 @@ export const Toast = forwardRef(
|
||||
zIndex: stackZIndex,
|
||||
};
|
||||
|
||||
// Back toasts (index > 0) use front toast's height when collapsed
|
||||
// Height is set via style (not animated) to avoid measurement issues
|
||||
// Only apply constraint after we've measured the natural height
|
||||
const shouldConstrainHeight =
|
||||
hasMeasured && !isExpanded && index > 0 && collapsedHeight;
|
||||
// Height animation for back toasts:
|
||||
// - Front toast (index 0): never set height, uses natural CSS height
|
||||
// - Back toasts: animate between collapsedHeight and their own naturalHeight
|
||||
const measuredHeight = naturalHeight || naturalHeightRef.current;
|
||||
const isBackToast = index > 0;
|
||||
const hasValidMeasurements =
|
||||
hasMeasured && collapsedHeight && measuredHeight;
|
||||
|
||||
// For back toasts with valid measurements, calculate target height
|
||||
// Otherwise, let CSS handle it naturally
|
||||
let animateProps: {
|
||||
opacity: number;
|
||||
y: number;
|
||||
scale: number;
|
||||
zIndex: number;
|
||||
height?: number;
|
||||
} = {
|
||||
opacity: 1,
|
||||
y: animateY,
|
||||
scale: animateScale,
|
||||
zIndex: stackZIndex,
|
||||
};
|
||||
|
||||
if (isBackToast && hasValidMeasurements) {
|
||||
animateProps.height = isExpanded ? measuredHeight : collapsedHeight;
|
||||
}
|
||||
|
||||
const shouldClipContent =
|
||||
isBackToast && hasValidMeasurements && !isExpanded;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -229,17 +256,11 @@ export const Toast = forwardRef(
|
||||
style={
|
||||
{
|
||||
'--toast-index': index,
|
||||
height: shouldConstrainHeight ? collapsedHeight : undefined,
|
||||
overflow: shouldConstrainHeight ? 'hidden' : undefined,
|
||||
overflow: shouldClipContent ? 'hidden' : undefined,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
initial={{ opacity: 0, y: 100, scale: 1 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: animateY,
|
||||
scale: animateScale,
|
||||
zIndex: stackZIndex,
|
||||
}}
|
||||
animate={animateProps}
|
||||
exit={exitAnimation}
|
||||
onAnimationComplete={definition => {
|
||||
// Clean up the manual close tracking after exit animation
|
||||
@@ -247,14 +268,14 @@ export const Toast = forwardRef(
|
||||
manuallyClosingToasts.delete(toast.key);
|
||||
}
|
||||
}}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 35 }}
|
||||
{...dataAttributes}
|
||||
data-status={finalStatus}
|
||||
{...restProps}
|
||||
>
|
||||
<div className={classes.content}>
|
||||
<div className={classes.wrapper}>
|
||||
{statusIcon && <div className={classes.icon}>{statusIcon}</div>}
|
||||
<div>
|
||||
<div className={classes.content}>
|
||||
<div {...titleProps} className={classes.title}>
|
||||
{content.title}
|
||||
</div>
|
||||
|
||||
+17
-15
@@ -18,31 +18,31 @@ import { forwardRef, Ref, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import { useToastRegion } from '@react-aria/toast';
|
||||
import { useToastQueue } from 'react-stately';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import type { ToastRegionProps } from './types';
|
||||
import type { ToastContainerProps } from './types';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { useInvertedThemeMode } from '../../hooks/useInvertedThemeMode';
|
||||
import { ToastRegionDefinition } from './definition';
|
||||
import { ToastContainerDefinition } from './definition';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
/**
|
||||
* A ToastRegion displays one or more toast notifications in the bottom-right corner.
|
||||
* A ToastContainer displays one or more toast notifications in the bottom-right corner.
|
||||
*
|
||||
* @remarks
|
||||
* The ToastRegion component should typically be placed once at the root of your application.
|
||||
* The ToastContainer component should typically be placed once at the root of your application.
|
||||
* It manages the display and stacking of all toast notifications added to its queue.
|
||||
* Toasts appear in the bottom-right corner with deep stacking when multiple are visible.
|
||||
* Toast regions are ARIA landmark regions that can be navigated using F6 (forward) and
|
||||
* Toast containers are ARIA landmark regions that can be navigated using F6 (forward) and
|
||||
* Shift+F6 (backward) for keyboard accessibility.
|
||||
*
|
||||
* @example
|
||||
* Basic setup in app root:
|
||||
* ```tsx
|
||||
* import { ToastRegion, toastQueue } from '@backstage/ui';
|
||||
* import { ToastContainer, toastQueue } from '@backstage/ui';
|
||||
*
|
||||
* function App() {
|
||||
* return (
|
||||
* <>
|
||||
* <ToastRegion queue={toastQueue} />
|
||||
* <ToastContainer queue={toastQueue} />
|
||||
* <YourAppContent />
|
||||
* </>
|
||||
* );
|
||||
@@ -51,10 +51,10 @@ import { Toast } from './Toast';
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ToastRegion = forwardRef(
|
||||
(props: ToastRegionProps, ref: Ref<HTMLDivElement>) => {
|
||||
export const ToastContainer = forwardRef(
|
||||
(props: ToastContainerProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
ToastRegionDefinition,
|
||||
ToastContainerDefinition,
|
||||
props,
|
||||
);
|
||||
const { classes, queue, className } = ownProps;
|
||||
@@ -64,10 +64,11 @@ export const ToastRegion = forwardRef(
|
||||
|
||||
// Use internal ref if none provided
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const regionRef = (ref as React.RefObject<HTMLDivElement>) || internalRef;
|
||||
const containerRef =
|
||||
(ref as React.RefObject<HTMLDivElement>) || internalRef;
|
||||
|
||||
// Get ARIA props for the toast region
|
||||
const { regionProps } = useToastRegion({}, state, regionRef);
|
||||
const { regionProps } = useToastRegion({}, state, containerRef);
|
||||
|
||||
// Track hover state for expanding/collapsing the stack
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
@@ -138,8 +139,8 @@ export const ToastRegion = forwardRef(
|
||||
return (
|
||||
<div
|
||||
{...regionProps}
|
||||
ref={regionRef}
|
||||
className={className || classes.region}
|
||||
ref={containerRef}
|
||||
className={className || classes.container}
|
||||
data-theme-mode={invertedThemeMode}
|
||||
data-hover-locked={isHoverLocked ? '' : undefined}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
@@ -160,6 +161,7 @@ export const ToastRegion = forwardRef(
|
||||
onClose={handleClose}
|
||||
expandedY={expandedYPositions[toast.key] ?? 0}
|
||||
collapsedHeight={index > 0 ? frontToastHeight : undefined}
|
||||
naturalHeight={toastHeights[toast.key]}
|
||||
onHeightChange={handleHeightChange}
|
||||
/>
|
||||
))}
|
||||
@@ -169,4 +171,4 @@ export const ToastRegion = forwardRef(
|
||||
},
|
||||
);
|
||||
|
||||
ToastRegion.displayName = 'ToastRegion';
|
||||
ToastContainer.displayName = 'ToastContainer';
|
||||
@@ -15,17 +15,18 @@
|
||||
*/
|
||||
|
||||
import { defineComponent } from '../../hooks/useDefinition';
|
||||
import type { ToastOwnProps, ToastRegionOwnProps } from './types';
|
||||
import type { ToastOwnProps, ToastContainerOwnProps } from './types';
|
||||
import styles from './Toast.module.css';
|
||||
|
||||
/**
|
||||
* Component definition for Toast
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
export const ToastDefinition = defineComponent<ToastOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-Toast',
|
||||
wrapper: 'bui-ToastWrapper',
|
||||
content: 'bui-ToastContent',
|
||||
title: 'bui-ToastTitle',
|
||||
description: 'bui-ToastDescription',
|
||||
@@ -52,16 +53,17 @@ export const ToastDefinition = defineComponent<ToastOwnProps>()({
|
||||
});
|
||||
|
||||
/**
|
||||
* Component definition for ToastRegion
|
||||
* Component definition for ToastContainer
|
||||
* @public
|
||||
*/
|
||||
export const ToastRegionDefinition = defineComponent<ToastRegionOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
region: 'bui-ToastRegion',
|
||||
},
|
||||
propDefs: {
|
||||
queue: {},
|
||||
className: {},
|
||||
},
|
||||
});
|
||||
export const ToastContainerDefinition =
|
||||
defineComponent<ToastContainerOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
container: 'bui-ToastContainer',
|
||||
},
|
||||
propDefs: {
|
||||
queue: {},
|
||||
className: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,14 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Toast } from './Toast';
|
||||
export { ToastRegion } from './ToastRegion';
|
||||
export { ToastDefinition, ToastRegionDefinition } from './definition';
|
||||
export { ToastContainer } from './ToastContainer';
|
||||
export { ToastContainerDefinition } from './definition';
|
||||
export { toastQueue } from './ToastQueue';
|
||||
export type {
|
||||
ToastContent,
|
||||
ToastOwnProps,
|
||||
ToastProps,
|
||||
ToastRegionOwnProps,
|
||||
ToastRegionProps,
|
||||
ToastContainerOwnProps,
|
||||
ToastContainerProps,
|
||||
} from './types';
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface ToastContent {
|
||||
|
||||
/**
|
||||
* Own props for the Toast component
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
export type ToastOwnProps = ContainerSurfaceProps & {
|
||||
/** Toast object from the queue */
|
||||
@@ -64,15 +64,15 @@ export type ToastOwnProps = ContainerSurfaceProps & {
|
||||
|
||||
/**
|
||||
* Properties for {@link Toast}
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
export interface ToastProps extends ToastOwnProps {}
|
||||
|
||||
/**
|
||||
* Own props for the ToastRegion component
|
||||
* Own props for the ToastContainer component
|
||||
* @public
|
||||
*/
|
||||
export type ToastRegionOwnProps = {
|
||||
export type ToastContainerOwnProps = {
|
||||
/** Toast queue instance */
|
||||
queue: ToastQueue<ToastContent>;
|
||||
/** Custom class name */
|
||||
@@ -80,7 +80,7 @@ export type ToastRegionOwnProps = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties for {@link ToastRegion}
|
||||
* Properties for {@link ToastContainer}
|
||||
* @public
|
||||
*/
|
||||
export interface ToastRegionProps extends ToastRegionOwnProps {}
|
||||
export interface ToastContainerProps extends ToastContainerOwnProps {}
|
||||
|
||||
@@ -55,7 +55,7 @@ export { SkeletonDefinition } from './components/Skeleton/definition';
|
||||
export { SwitchDefinition } from './components/Switch/definition';
|
||||
export {
|
||||
ToastDefinition,
|
||||
ToastRegionDefinition,
|
||||
ToastContainerDefinition,
|
||||
} from './components/Toast/definition';
|
||||
export { ToggleButtonDefinition } from './components/ToggleButton/definition';
|
||||
export { ToggleButtonGroupDefinition } from './components/ToggleButtonGroup/definition';
|
||||
|
||||
Reference in New Issue
Block a user