155 lines
3.7 KiB
JavaScript
155 lines
3.7 KiB
JavaScript
/**
|
|
* External dependencies
|
|
*/
|
|
import { noop } from 'lodash';
|
|
import classnames from 'classnames';
|
|
import { speak } from '@wordpress/a11y';
|
|
import { useEffect, forwardRef, renderToString } from '@wordpress/element';
|
|
import { __ } from '@wordpress/i18n';
|
|
import warning from '@wordpress/warning';
|
|
import { Button } from '@wordpress/components';
|
|
|
|
const NOTICE_TIMEOUT = 10000;
|
|
|
|
/** @typedef {import('@wordpress/element').WPElement} WPElement */
|
|
|
|
/**
|
|
* Custom hook which announces the message with the given politeness, if a
|
|
* valid message is provided.
|
|
*
|
|
* @param {string|WPElement} [message] Message to announce.
|
|
* @param {'polite'|'assertive'} politeness Politeness to announce.
|
|
*/
|
|
function useSpokenMessage( message, politeness ) {
|
|
const spokenMessage =
|
|
typeof message === 'string' ? message : renderToString( message );
|
|
|
|
useEffect( () => {
|
|
if ( spokenMessage ) {
|
|
speak( spokenMessage, politeness );
|
|
}
|
|
}, [ spokenMessage, politeness ] );
|
|
}
|
|
|
|
function Snackbar(
|
|
{
|
|
className,
|
|
children,
|
|
spokenMessage = children,
|
|
politeness = 'polite',
|
|
actions = [],
|
|
onRemove = noop,
|
|
icon = null,
|
|
explicitDismiss = false,
|
|
// onDismiss is a callback executed when the snackbar is dismissed.
|
|
// It is distinct from onRemove, which _looks_ like a callback but is
|
|
// actually the function to call to remove the snackbar from the UI.
|
|
onDismiss = null,
|
|
},
|
|
ref
|
|
) {
|
|
onDismiss = onDismiss || noop;
|
|
|
|
function dismissMe( event ) {
|
|
if ( event && event.preventDefault ) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
onDismiss();
|
|
onRemove();
|
|
}
|
|
|
|
function onActionClick( event, onClick ) {
|
|
event.stopPropagation();
|
|
|
|
if ( explicitDismiss ) {
|
|
onRemove();
|
|
}
|
|
|
|
if ( onClick ) {
|
|
onClick( event );
|
|
}
|
|
}
|
|
|
|
useSpokenMessage( spokenMessage, politeness );
|
|
|
|
// Only set up the timeout dismiss if we're not explicitly dismissing.
|
|
useEffect( () => {
|
|
const timeoutHandle = setTimeout( () => {
|
|
if ( ! explicitDismiss ) {
|
|
onDismiss();
|
|
onRemove();
|
|
}
|
|
}, NOTICE_TIMEOUT );
|
|
|
|
return () => clearTimeout( timeoutHandle );
|
|
}, [ onDismiss, onRemove ] );
|
|
|
|
const classes = classnames( className, 'components-snackbar', {
|
|
'components-snackbar-explicit-dismiss': !! explicitDismiss,
|
|
} );
|
|
if ( actions && actions.length > 1 ) {
|
|
// we need to inform developers that snackbar only accepts 1 action
|
|
warning(
|
|
'Snackbar can only have 1 action, use Notice if your message require many messages'
|
|
);
|
|
// return first element only while keeping it inside an array
|
|
actions = [ actions[ 0 ] ];
|
|
}
|
|
|
|
const snackbarContentClassnames = classnames(
|
|
'components-snackbar__content',
|
|
{
|
|
'components-snackbar__content-with-icon': !! icon,
|
|
}
|
|
);
|
|
|
|
return (
|
|
<div
|
|
ref={ ref }
|
|
className={ classes }
|
|
onClick={ ! explicitDismiss ? dismissMe : noop }
|
|
tabIndex="0"
|
|
role={ ! explicitDismiss ? 'button' : '' }
|
|
onKeyPress={ ! explicitDismiss ? dismissMe : noop }
|
|
aria-label={ ! explicitDismiss ? __( 'Dismiss this notice' ) : '' }
|
|
>
|
|
<div className={ snackbarContentClassnames }>
|
|
{ icon && (
|
|
<div className="components-snackbar__icon">{ icon }</div>
|
|
) }
|
|
{ children }
|
|
{ actions.map( ( { label, onClick, url }, index ) => {
|
|
return (
|
|
<Button
|
|
key={ index }
|
|
href={ url }
|
|
isTertiary
|
|
onClick={ ( event ) =>
|
|
onActionClick( event, onClick )
|
|
}
|
|
className="components-snackbar__action"
|
|
>
|
|
{ label }
|
|
</Button>
|
|
);
|
|
} ) }
|
|
{ explicitDismiss && (
|
|
<span
|
|
role="button"
|
|
aria-label="Dismiss this notice"
|
|
tabIndex="0"
|
|
className="components-snackbar__dismiss-button"
|
|
onClick={ dismissMe }
|
|
onKeyPress={ dismissMe }
|
|
>
|
|
✕
|
|
</span>
|
|
) }
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default forwardRef( Snackbar );
|