mirror of https://github.com/mastodon/mastodon
				
				
				
			Create reusable Alert/Snackbar component (#36141)
							parent
							
								
									db0cd9489c
								
							
						
					
					
						commit
						085e9ea676
					
				@ -0,0 +1,2 @@
 | 
			
		||||
<html class="no-reduce-motion">
 | 
			
		||||
</html>
 | 
			
		||||
@ -0,0 +1,110 @@
 | 
			
		||||
import type { Meta, StoryObj } from '@storybook/react-vite';
 | 
			
		||||
import { fn, expect } from 'storybook/test';
 | 
			
		||||
 | 
			
		||||
import { Alert } from '.';
 | 
			
		||||
 | 
			
		||||
const meta = {
 | 
			
		||||
  title: 'Components/Alert',
 | 
			
		||||
  component: Alert,
 | 
			
		||||
  args: {
 | 
			
		||||
    isActive: true,
 | 
			
		||||
    animateFrom: 'side',
 | 
			
		||||
    title: '',
 | 
			
		||||
    message: '',
 | 
			
		||||
    action: '',
 | 
			
		||||
    onActionClick: fn(),
 | 
			
		||||
  },
 | 
			
		||||
  argTypes: {
 | 
			
		||||
    isActive: {
 | 
			
		||||
      control: 'boolean',
 | 
			
		||||
      type: 'boolean',
 | 
			
		||||
      description: 'Animate to the active (displayed) state of the alert',
 | 
			
		||||
    },
 | 
			
		||||
    animateFrom: {
 | 
			
		||||
      control: 'radio',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      options: ['side', 'below'],
 | 
			
		||||
      description:
 | 
			
		||||
        'Direction that the alert animates in from when activated. `side` is dependent on reading direction, defaulting to left in ltr languages.',
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
      control: 'text',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      description: '(Optional) title of the alert',
 | 
			
		||||
    },
 | 
			
		||||
    message: {
 | 
			
		||||
      control: 'text',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      description: 'Main alert text',
 | 
			
		||||
    },
 | 
			
		||||
    action: {
 | 
			
		||||
      control: 'text',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      description:
 | 
			
		||||
        'Label of the alert action (requires `onActionClick` handler)',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  tags: ['test'],
 | 
			
		||||
} satisfies Meta<typeof Alert>;
 | 
			
		||||
 | 
			
		||||
export default meta;
 | 
			
		||||
 | 
			
		||||
type Story = StoryObj<typeof meta>;
 | 
			
		||||
 | 
			
		||||
export const Simple: Story = {
 | 
			
		||||
  args: {
 | 
			
		||||
    message: 'Post published.',
 | 
			
		||||
  },
 | 
			
		||||
  render: (args) => (
 | 
			
		||||
    <div style={{ overflow: 'clip', padding: '1rem' }}>
 | 
			
		||||
      <Alert {...args} />
 | 
			
		||||
    </div>
 | 
			
		||||
  ),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const WithAction: Story = {
 | 
			
		||||
  args: {
 | 
			
		||||
    ...Simple.args,
 | 
			
		||||
    action: 'Open',
 | 
			
		||||
  },
 | 
			
		||||
  render: Simple.render,
 | 
			
		||||
  play: async ({ args, canvas, userEvent }) => {
 | 
			
		||||
    const button = await canvas.findByRole('button', { name: 'Open' });
 | 
			
		||||
    await userEvent.click(button);
 | 
			
		||||
    await expect(args.onActionClick).toHaveBeenCalled();
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const WithTitle: Story = {
 | 
			
		||||
  args: {
 | 
			
		||||
    title: 'Warning:',
 | 
			
		||||
    message: 'This is an alert',
 | 
			
		||||
  },
 | 
			
		||||
  render: Simple.render,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const WithDismissButton: Story = {
 | 
			
		||||
  args: {
 | 
			
		||||
    message: 'More replies found',
 | 
			
		||||
    action: 'Show',
 | 
			
		||||
    onDismiss: fn(),
 | 
			
		||||
  },
 | 
			
		||||
  render: Simple.render,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const InSizedContainer: Story = {
 | 
			
		||||
  args: WithDismissButton.args,
 | 
			
		||||
  render: (args) => (
 | 
			
		||||
    <div
 | 
			
		||||
      style={{
 | 
			
		||||
        overflow: 'clip',
 | 
			
		||||
        padding: '1rem',
 | 
			
		||||
        width: '380px',
 | 
			
		||||
        maxWidth: '100%',
 | 
			
		||||
        boxSizing: 'border-box',
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Alert {...args} />
 | 
			
		||||
    </div>
 | 
			
		||||
  ),
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
import { useIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
 | 
			
		||||
 | 
			
		||||
import { IconButton } from '../icon_button';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Snackbar/Toast-style notification component.
 | 
			
		||||
 */
 | 
			
		||||
export const Alert: React.FC<{
 | 
			
		||||
  isActive?: boolean;
 | 
			
		||||
  animateFrom?: 'side' | 'below';
 | 
			
		||||
  title?: string;
 | 
			
		||||
  message: string;
 | 
			
		||||
  action?: string;
 | 
			
		||||
  onActionClick?: () => void;
 | 
			
		||||
  onDismiss?: () => void;
 | 
			
		||||
}> = ({
 | 
			
		||||
  isActive,
 | 
			
		||||
  animateFrom = 'side',
 | 
			
		||||
  title,
 | 
			
		||||
  message,
 | 
			
		||||
  action,
 | 
			
		||||
  onActionClick,
 | 
			
		||||
  onDismiss,
 | 
			
		||||
}) => {
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
 | 
			
		||||
  const hasAction = Boolean(action && onActionClick);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames('notification-bar', {
 | 
			
		||||
        'notification-bar--active': isActive,
 | 
			
		||||
        'from-side': animateFrom === 'side',
 | 
			
		||||
        'from-below': animateFrom === 'below',
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
      <span className='notification-bar__content'>
 | 
			
		||||
        {Boolean(title) && (
 | 
			
		||||
          <span className='notification-bar__title'>{title}</span>
 | 
			
		||||
        )}
 | 
			
		||||
        {message}
 | 
			
		||||
      </span>
 | 
			
		||||
 | 
			
		||||
      {hasAction && (
 | 
			
		||||
        <button className='notification-bar__action' onClick={onActionClick}>
 | 
			
		||||
          {action}
 | 
			
		||||
        </button>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {onDismiss && (
 | 
			
		||||
        <IconButton
 | 
			
		||||
          title={intl.formatMessage({
 | 
			
		||||
            id: 'dismissable_banner.dismiss',
 | 
			
		||||
            defaultMessage: 'Dismiss',
 | 
			
		||||
          })}
 | 
			
		||||
          icon='times'
 | 
			
		||||
          iconComponent={CloseIcon}
 | 
			
		||||
          className='notification-bar__dismiss-button'
 | 
			
		||||
          onClick={onDismiss}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue