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
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- `Popover`: Widen `offset` to also accept an object with separate main and cross axis offsets. The same applies to `BorderBoxControl`'s `popoverOffset` prop ([#82060](https://github.com/WordPress/gutenberg/pull/82060)).

### Bug Fixes

- `Snackbar`: Restart the auto-dismiss timer when a notice is recreated with the same ID during removal ([#81764](https://github.com/WordPress/gutenberg/pull/81764)).
Comment thread
maxschmeling marked this conversation as resolved.

### Internal

- Use the `.jsx` extension for JavaScript source files that contain JSX ([#80990](https://github.com/WordPress/gutenberg/pull/80990)).
Expand Down
16 changes: 10 additions & 6 deletions packages/components/src/snackbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ForwardedRef, KeyboardEvent, MouseEvent } from 'react';
import clsx from 'clsx';
import { useIsPresent } from 'framer-motion';
import { speak } from '@wordpress/a11y';
import {
useEffect,
Expand Down Expand Up @@ -57,6 +58,8 @@ function UnforwardedSnackbar(
}: WordPressComponentProps< SnackbarProps, 'div' >,
ref: ForwardedRef< any >
) {
const isPresent = useIsPresent();

function dismissMe( event: KeyboardEvent | MouseEvent ) {
if ( event && event.preventDefault ) {
event.preventDefault();
Expand Down Expand Up @@ -92,16 +95,17 @@ function UnforwardedSnackbar(
} );

useEffect( () => {
// Only set up the timeout dismiss if we're not explicitly dismissing.
if ( explicitDismiss || ! isPresent ) {
return;
}

const timeoutHandle = setTimeout( () => {
if ( ! explicitDismiss ) {
callbacksRef.current.onDismiss?.();
callbacksRef.current.onRemove?.();
}
callbacksRef.current.onDismiss?.();
callbacksRef.current.onRemove?.();
}, NOTICE_TIMEOUT );

return () => clearTimeout( timeoutHandle );
}, [ explicitDismiss ] );
}, [ explicitDismiss, isPresent ] );

const classes = clsx( className, 'components-snackbar', {
'components-snackbar-explicit-dismiss': !! explicitDismiss,
Expand Down
30 changes: 28 additions & 2 deletions packages/components/src/snackbar/test/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, within } from '@testing-library/react';
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { click } from '@ariakit/test';
import { speak } from '@wordpress/a11y';
import { SVG, Path } from '@wordpress/primitives';
Expand All @@ -14,6 +14,10 @@ describe( 'Snackbar', () => {
mockedSpeak.mockReset();
} );

afterEach( () => {
jest.useRealTimers();
} );

it( 'should render correctly', () => {
render( <Snackbar>Message</Snackbar> );

Expand Down Expand Up @@ -44,6 +48,22 @@ describe( 'Snackbar', () => {
expect( icon ).toBeVisible();
} );

it( 'should not restart auto-dismissal after an unrelated rerender', async () => {
jest.useFakeTimers();
const removeNotice = jest.fn();
const { rerender } = render(
<Snackbar onRemove={ () => removeNotice() }>Message</Snackbar>
);

await act( async () => jest.advanceTimersByTime( 5000 ) );
rerender(
<Snackbar onRemove={ () => removeNotice() }>Message</Snackbar>
);
await act( async () => jest.advanceTimersByTime( 1000 ) );

expect( removeNotice ).toHaveBeenCalledTimes( 1 );
} );

it( 'should be dismissible by clicking the snackbar', async () => {
const onRemove = jest.fn();
const onDismiss = jest.fn();
Expand All @@ -69,6 +89,7 @@ describe( 'Snackbar', () => {
} );

it( 'should not be dismissible by clicking the snackbar when the `explicitDismiss` prop is set to `true`', async () => {
jest.useFakeTimers();
const onRemove = jest.fn();
const onDismiss = jest.fn();

Expand All @@ -93,7 +114,12 @@ describe( 'Snackbar', () => {
'components-snackbar-explicit-dismiss'
);

await click( snackbar );
fireEvent.click( snackbar );

expect( onRemove ).not.toHaveBeenCalled();
expect( onDismiss ).not.toHaveBeenCalled();

await act( async () => jest.advanceTimersByTime( 6000 ) );

expect( onRemove ).not.toHaveBeenCalled();
expect( onDismiss ).not.toHaveBeenCalled();
Expand Down
62 changes: 61 additions & 1 deletion packages/components/src/snackbar/test/list.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { render, screen } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import { click } from '@ariakit/test';
import { useEffect, useState } from '@wordpress/element';
import SnackbarList from '../list';

jest.mock( '@wordpress/compose', () => ( {
...jest.requireActual( '@wordpress/compose' ),
useReducedMotion: () => false,
} ) );

window.scrollTo = jest.fn();

describe( 'SnackbarList', () => {
afterEach( () => {
jest.resetAllMocks();
jest.useRealTimers();
} );

it( 'should get focus after a snackbar is dismissed', async () => {
Expand Down Expand Up @@ -36,4 +43,57 @@ describe( 'SnackbarList', () => {

expect( screen.getByTestId( 'snackbar-list' ) ).toHaveFocus();
} );

it( 'should restart auto-dismissal when a notice is replaced with the same ID', async () => {
jest.useFakeTimers();
const onRemove = jest.fn();
const notice = {
id: 'ID_1',
content: 'A collaborator joined.',
};

function RecreatedNotice() {
const [ notices, setNotices ] = useState( [ notice ] );
const [ shouldRecreate, setShouldRecreate ] = useState( false );

useEffect( () => {
if ( ! shouldRecreate ) {
return;
}

const timeoutHandle = setTimeout( () => {
setNotices( [ { ...notice } ] );
setShouldRecreate( false );
}, 50 );

return () => clearTimeout( timeoutHandle );
}, [ shouldRecreate ] );

return (
<SnackbarList
notices={ notices }
onRemove={ ( id ) => {
onRemove( id );
setNotices( [] );
setShouldRecreate( true );
} }
/>
);
}

render( <RecreatedNotice /> );

await act( async () => jest.advanceTimersByTime( 6000 ) );
expect( onRemove ).toHaveBeenCalledTimes( 1 );

// Recreate the notice before its 100ms exit animation completes.
await act( async () => jest.advanceTimersByTime( 50 ) );
Comment thread
maxschmeling marked this conversation as resolved.

expect( screen.getByTestId( 'snackbar' ) ).toHaveTextContent(
notice.content
);

await act( async () => jest.advanceTimersByTime( 6000 ) );
expect( onRemove ).toHaveBeenCalledTimes( 2 );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ test.describe( 'Collaboration - Notification preferences', () => {
// the admin editor show the join notice.
await collaborationUtils.openCollaborativeSession( post.id );

// Scope to the last snackbar so a stray/earlier snackbar can't
// cause a Playwright strict-mode violation on this locator.
await expect( page.getByTestId( 'snackbar' ).last() ).toContainText(
`${ SECOND_USER_DISPLAY_NAME } has joined the post.`,
{ timeout: 10000 }
);
const joinNoticeText = `${ SECOND_USER_DISPLAY_NAME } has joined the post.`;
const joinNotice = page
.getByTestId( 'snackbar' )
.filter( { hasText: joinNoticeText } );
await expect( joinNotice ).toContainText( joinNoticeText, {
timeout: 10000,
} );
await expect( joinNotice ).toBeHidden( { timeout: 10000 } );
} );
} );
Loading