Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion frontend/src/components/ui/NotificationSystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ interface NotificationSystemProps {
onDismiss: () => void;
/** Auto-dismiss timeout in milliseconds (default: 3000ms, 0 to disable) */
autoDismissMs?: number;
/**
* Drop the toast clear of a full-width 48px bar pinned to the top of the
* viewport. Set it while such a bar is up — the localize object editor and
* the add-object overlay both raise one, and both end it with a close
* button on the same right edge the toast anchors to.
*/
belowTopBar?: boolean;
}

/**
Expand Down Expand Up @@ -68,6 +75,7 @@ export const NotificationSystem: React.FC<NotificationSystemProps> = ({
toastType,
onDismiss,
autoDismissMs = 3000,
belowTopBar = false,
}) => {
// Auto-dismiss logic
useEffect(() => {
Expand All @@ -86,7 +94,11 @@ export const NotificationSystem: React.FC<NotificationSystemProps> = ({

return (
<div
className={`fixed top-4 right-4 z-50 transition-all duration-300 ease-in-out transform ${
// top-16 clears a 48px top bar with the same 16px of air top-4 leaves
// against the viewport edge. It is not the default because the pages
// that raise such a bar only raise it some of the time, and 64px is
// where their own content — the localize rail's header button — starts.
className={`fixed ${belowTopBar ? 'top-16' : 'top-4'} right-4 z-50 transition-all duration-300 ease-in-out transform ${
showToast ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
}`}
>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/pages/LocalizeAlertPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2498,6 +2498,11 @@ export default function LocalizeAlertPage({ mode }: LocalizeAlertPageProps = {})
toastMessage={toastMessage}
toastType={toastType}
onDismiss={dismissToast}
// Both full-screen overlays fill the top 48px of the viewport with
// their own bar, close button included; the toast has to clear it.
// With neither up it stays high, where it covers only the page
// header's progress badge rather than the rail's FP toggle.
belowTopBar={modalContext !== null || addObjectOpen}
/>

{showShortcutsModal && (
Expand Down
19 changes: 18 additions & 1 deletion frontend/tests/components/ui/NotificationSystem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,26 @@ describe('NotificationSystem', () => {
expect(screen.getByText(longMessage)).toBeInTheDocument();
});

it('should sit high by default', () => {
const { container } = render(<NotificationSystem {...defaultProps} />);

expect(container.querySelector('.fixed.right-4')).toHaveClass('top-4');
expect(container.querySelector('.top-16')).toBeNull();
});

it('should clear a full-width top bar when asked', () => {
// top-16 (64px) clears the 48px bar the localize object editor and the
// add-object overlay each pin to the top of the viewport — a bar whose
// close button sits at the same right edge the toast does.
const { container } = render(<NotificationSystem {...defaultProps} belowTopBar />);

expect(container.querySelector('.fixed.right-4')).toHaveClass('top-16');
expect(container.querySelector('.top-4')).toBeNull();
});

it('should apply correct transition classes', () => {
const { container } = render(<NotificationSystem {...defaultProps} />);

const notificationDiv = container.querySelector('.fixed.top-4.right-4');
expect(notificationDiv).toHaveClass(
'transition-all',
Expand Down
34 changes: 34 additions & 0 deletions frontend/tests/pages/LocalizeAlertPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,40 @@ describe('LocalizeAlertPage', () => {
expect(screen.queryByText('Frame saved')).not.toBeInTheDocument();
});

it('drops the toast below the editor bar while the editor is open, and only then', async () => {
// The editor's own 48px bar ends with its close button on the right edge
// the toast anchors to; with no editor up, that height belongs to the
// rail's header button instead.
vi.mocked(apiClient.createDetectionAnnotation).mockRejectedValue(new Error('boom'));
await renderAndSettle(<LocalizeAlertPage />, {
wrapper: makeWrapper(`${ROUTES.LOCALIZE}/101/object/101/1001`),
});
await waitFor(() => expect(screen.getByTestId('image-modal')).toBeInTheDocument());

fireEvent.click(screen.getByText('Mock Submit'));

const toast = (await screen.findByText(/failed to save frame/i)).closest('.fixed')!;
expect(toast).toHaveClass('top-16');
expect(toast).not.toHaveClass('top-4');
});

it('keeps the toast high when no overlay owns the top of the viewport', async () => {
vi.mocked(apiClient.bulkUpsertDetectionAnnotations).mockRejectedValue(new Error('boom'));
await renderAndSettle(<LocalizeAlertPage />, { wrapper });

fireEvent.click(screen.getByRole('button', { name: 'Object 2' }));
fireEvent.click(
within(screen.getByTestId('localize-active-object-actions')).getByRole('button', {
name: "Accept Object 2's boxes",
})
);
fireEvent.click(await screen.findByTestId('accept-remaining-confirm'));

const toast = (await screen.findByText(/failed to accept boxes/i)).closest('.fixed')!;
expect(toast).toHaveClass('top-4');
expect(toast).not.toHaveClass('top-16');
});

it('hands the editor the object named in the URL, not just the frame', async () => {
await renderAndSettle(<LocalizeAlertPage />, {
wrapper: makeWrapper(`${ROUTES.LOCALIZE}/101/object/101/1001`),
Expand Down
Loading