refactor(payments): Convert AlertBar component to utilize Tailwind

This commit is contained in:
Lisa Chan 2022-10-18 21:16:41 -04:00
Родитель c26f311240
Коммит c9422731eb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: FB36CA729316F2F9
13 изменённых файлов: 275 добавлений и 165 удалений

Просмотреть файл

@ -1,68 +0,0 @@
@import '../../../../fxa-content-server/app/styles/breakpoints';
.portal#top-bar {
padding: 0 32px;
@include respond-to('small') {
padding: 0;
}
}
#top-bar .alert {
background: rgba(0, 0, 0, 0.1);
font-weight: 400;
font-size: 13px;
line-height: 21px;
margin: 4px auto;
min-height: 2em;
padding: 0.5em;
text-align: center;
}
#top-bar .alertError,
#top-bar .alertSuccess {
background: #1d1133;
color: #fff;
.close-alert-bar {
cursor: pointer;
display: inline-block;
position: absolute;
right: 20px;
path {
fill: #fff;
}
}
}
#top-bar .alertError {
background: #FF4F5E;
}
#top-bar .newsletter-error {
color: #3D3D3D;
background: #FFA436;
}
.alert span.checked::before {
background-image: url('../../images/check.svg');
background-position: 0 3px;
background-repeat: no-repeat;
content: '\00a0';
display: inline-block;
height: 16px;
margin: 0 4px;
width: 16px;
}
.alertCenter {
border-radius: 6px;
margin: 0 auto;
position: relative;
width: 100%;
@include respond-to('big') {
max-width: 640px;
}
}

Просмотреть файл

@ -1,14 +1,127 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { AlertBar } from './index';
import { Meta } from '@storybook/react';
import { AlertBar, AlertBarProps } from './index';
import MockApp from '../../../.storybook/components/MockApp';
import { SettingsLayout } from '../AppLayout';
storiesOf('components/AlertBar', module).add('basic', () => (
<AlertBar
className="alert"
dataTestId="alert-bar"
headerId="alert-bar-header"
localizedId="alert-bar"
>
This is an alert.
</AlertBar>
));
export default {
title: 'components/AlertBar',
component: AlertBar,
} as Meta;
const storyWithProps = ({
actionButton,
checked,
children,
className,
dataTestId,
elems,
headerId,
localizedId,
onClick,
}: AlertBarProps) => {
const story = () => (
<MockApp>
<SettingsLayout>
<AlertBar
actionButton={actionButton}
checked={checked}
className={className}
dataTestId={dataTestId}
elems={elems}
headerId={headerId}
localizedId={localizedId}
onClick={onClick}
>
{children}
</AlertBar>
<p style={{ padding: '0 2em 4em 2em' }}>App content goes here</p>
</SettingsLayout>
</MockApp>
);
return story;
};
export const ErrorAlert = storyWithProps({
children: 'Invalid payment information; there is an error with your account.',
className: 'alert-error',
dataTestId: 'error-alert',
elems: true,
headerId: 'error-alert-bar-header',
localizedId: 'alert-bar-error',
});
export const ErrorAlertAction = storyWithProps({
actionButton: () => {},
children:
'Invalid payment information; there is an error with your account. This alert may take some time to clear after you successfully update your information. {actionButton}',
className: 'alert-error',
dataTestId: 'invalid-payment-error-pending',
elems: true,
headerId: 'sub-route-funding-source-payment-alert-header',
localizedId: 'sub-route-funding-source-payment-alert',
});
export const PendingAlert = storyWithProps({
children: 'Updating billing information…',
className: 'alert-pending',
dataTestId: 'alert-pending',
headerId: 'pending-alert-bar-header',
localizedId: 'alert-bar-pending',
});
export const NewsletterErrorAlertBar = storyWithProps({
children:
'Youre not signed up for product update emails. You can try again in your account settings.',
className: 'alert-newsletter-error',
dataTestId: 'alert-newsletter',
headerId: 'newsletter-alert-bar-header',
localizedId: 'alert-bar-newsletter',
});
export const ShortSuccessAlertBar = storyWithProps({
children: 'Success!',
className: 'alert-success',
dataTestId: 'alert-success-short',
headerId: 'short-success-alert-bar-header',
localizedId: 'short-alert-bar',
});
export const ShortSuccessAlertBarClose = storyWithProps({
children: 'Success!',
className: 'alert-success',
dataTestId: 'alert-success-short-close',
headerId: 'short-success-alert-bar-header-close',
localizedId: 'short-alert-bar-close',
onClick: () => {},
});
export const LongSuccessAlertBar = storyWithProps({
children:
'Spicy jalapeno bacon ipsum dolor amet voluptate pariatur cupim anim, laboris alcatra biltong swine meatball fatback short loin shankle ea fugiat. Deserunt pork filet mignon, elit in est chicken. Dolore salami minim et. Leberkas chislic laborum cillum cow. Officia occaecat chuck enim, chislic eiusmod t-bone. Adipisicing labore veniam porchetta est rump. Occaecat aute pariatur salami alcatra chislic sunt velit tri-tip aliqua kielbasa mollit beef ribs. Pastrami labore salami ipsum eu laboris, filet mignon enim tenderloin excepteur aliqua buffalo sint lorem. Do beef cupim, drumstick id venison ball tip et pork chop brisket boudin in sed. Tenderloin ball tip id proident ullamco lorem non!',
className: 'alert-success',
dataTestId: 'alert-success-long',
headerId: 'long-success-alert-bar-header',
localizedId: 'long-alert-bar',
});
export const LongSuccessAlertBarClose = storyWithProps({
children:
'Spicy jalapeno bacon ipsum dolor amet voluptate pariatur cupim anim, laboris alcatra biltong swine meatball fatback short loin shankle ea fugiat. Deserunt pork filet mignon, elit in est chicken. Dolore salami minim et. Leberkas chislic laborum cillum cow. Officia occaecat chuck enim, chislic eiusmod t-bone. Adipisicing labore veniam porchetta est rump. Occaecat aute pariatur salami alcatra chislic sunt velit tri-tip aliqua kielbasa mollit beef ribs. Pastrami labore salami ipsum eu laboris, filet mignon enim tenderloin excepteur aliqua buffalo sint lorem. Do beef cupim, drumstick id venison ball tip et pork chop brisket boudin in sed. Tenderloin ball tip id proident ullamco lorem non!',
className: 'alert-success',
dataTestId: 'alert-success-long-close',
headerId: 'long-success-alert-bar-header-close',
localizedId: 'long-alert-bar-close',
onClick: () => {},
});
export const LongSuccessAlertBarCheckClose = storyWithProps({
checked: true,
children:
'Spicy jalapeno bacon ipsum dolor amet voluptate pariatur cupim anim, laboris alcatra biltong swine meatball fatback short loin shankle ea fugiat. Deserunt pork filet mignon, elit in est chicken. Dolore salami minim et. Leberkas chislic laborum cillum cow. Officia occaecat chuck enim, chislic eiusmod t-bone. Adipisicing labore veniam porchetta est rump. Occaecat aute pariatur salami alcatra chislic sunt velit tri-tip aliqua kielbasa mollit beef ribs. Pastrami labore salami ipsum eu laboris, filet mignon enim tenderloin excepteur aliqua buffalo sint lorem. Do beef cupim, drumstick id venison ball tip et pork chop brisket boudin in sed. Tenderloin ball tip id proident ullamco lorem non!',
className: 'alert-success',
dataTestId: 'alert-success-long-close',
headerId: 'long-success-alert-bar-header-close',
localizedId: 'long-alert-bar-close',
onClick: () => {},
});

Просмотреть файл

@ -16,24 +16,72 @@ it('renders as expected', () => {
<div id="alert-bar-header">Message for mom</div>
</AlertBar>
);
expect(queryByTestId('alert-container')).toHaveClass('alert');
expect(queryByTestId('alert-container')).toHaveAttribute('aria-labelledby', "alert-bar-header");
expect(queryByTestId('alert-container')).toHaveAttribute('role', "dialog");
expect(queryByTestId('alert-container')).toHaveClass('bg-black/10');
expect(queryByTestId('alert-container')).toHaveAttribute(
'aria-labelledby',
'alert-bar-header'
);
expect(queryByTestId('alert-container')).toHaveAttribute('role', 'dialog');
});
it('renders success alert', () => {
const { queryByTestId } = render(
<AlertBar
className="alert-success"
dataTestId="children"
headerId="success-alert-bar-header"
localizedId="success-alert-bar"
>
<div id="success-alert-bar-header">Hi mom, this was a success</div>
</AlertBar>
);
expect(queryByTestId('alert-container')).toHaveClass(
'bg-grey-700 text-white'
);
expect(queryByTestId('children')).toBeInTheDocument();
});
it('accepts an alternate className', () => {
it('renders error alert', () => {
const { queryByTestId } = render(
<AlertBar
className="foo"
className="alert-error"
dataTestId="children"
headerId="alternate-alert-bar-header"
localizedId="alert-bar"
headerId="error-alert-bar-header"
localizedId="error-alert-bar"
>
<div id="alternate-alert-bar-header">Hi mom</div>
<div id="error-alert-bar-header">Hi mom, this has an error</div>
</AlertBar>
);
expect(queryByTestId('alert-container')).not.toHaveClass('alert');
expect(queryByTestId('alert-container')).toHaveClass('foo');
expect(queryByTestId('alert-container')).toHaveClass('bg-red-600 text-white');
expect(queryByTestId('children')).toBeInTheDocument();
});
it('renders newsletter error alert', () => {
const { queryByTestId } = render(
<AlertBar
className="alert-newsletter-error"
dataTestId="children"
headerId="newsletter-error-alert-bar-header"
localizedId="newsletter-error-alert-bar"
>
<div id="newsletter-alert-bar-header">Hi mom, we have newsletters?</div>
</AlertBar>
);
expect(queryByTestId('alert-container')).toHaveClass('bg-yellow-500');
expect(queryByTestId('children')).toBeInTheDocument();
});
it('renders pending alert', () => {
const { queryByTestId } = render(
<AlertBar
className="alert-pending"
dataTestId="children"
headerId="pending-alert-bar-header"
localizedId="pending-alert-bar"
>
<div id="pending-alert-bar-header">Waiting for mom...</div>
</AlertBar>
);
expect(queryByTestId('alert-container')).toHaveClass('bg-black/10');
expect(queryByTestId('children')).toBeInTheDocument();
});

Просмотреть файл

@ -5,10 +5,11 @@
import React from 'react';
import { ReactComponent as CloseIcon } from 'fxa-react/images/close.svg';
import { Localized } from '@fluent/react';
import classNames from 'classnames';
import Portal from 'fxa-react/components/Portal';
import './index.scss';
import checkLogo from '../../images/check.svg';
type AlertBarProps = {
export type AlertBarProps = {
actionButton?: any;
checked?: boolean;
children: any;
@ -24,48 +25,81 @@ export const AlertBar = ({
actionButton,
checked,
children,
className = 'alert',
className,
dataTestId,
elems,
headerId,
localizedId,
onClick,
}: AlertBarProps) => {
let alertTypeStyle;
switch (className) {
case 'alert-error':
alertTypeStyle = 'bg-red-600 text-white';
break;
case 'alert-newsletter-error':
alertTypeStyle = 'bg-yellow-500';
break;
case 'alert-success':
alertTypeStyle = 'bg-grey-700 text-white';
break;
case 'alert-pending':
default:
alertTypeStyle = 'bg-black/10';
break;
}
return (
<Portal id="top-bar">
<div
aria-labelledby={headerId}
className={className}
className={classNames(
'flex font-medium items-center justify-center leading-5 min-h-[32px] my-1 mx-auto p-2 relative rounded-md text-sm w-full tablet:max-w-[640px]',
alertTypeStyle
)}
data-testid="alert-container"
role="dialog"
>
<Localized id={localizedId} elems={elems ? { div: actionButton } : undefined}>
<span
id={headerId}
data-testid={dataTestId}
className={checked ? "checked" : undefined}
>
{children}
</span>
</Localized>
<div className="text-center w-[80%]">
{checked && (
<img
src={checkLogo}
className="h-4 my-0 mx-1 relative top-[3px] w-4"
alt=""
/>
)}
{onClick && <Localized id="close-aria">
<Localized
id={localizedId}
elems={elems ? { div: actionButton } : undefined}
>
<span id={headerId} data-testid={dataTestId}>
{children}
</span>
</Localized>
</div>
{onClick && (
<Localized id="close-aria">
<span
data-testid="clear-success-alert"
className="close"
className="grid"
aria-label="Close modal"
onClick={() => onClick()}
role="button"
>
<CloseIcon
role="img"
className="close-icon close-alert-bar"
className="w-4 h-4 absolute cursor-pointer fill-current justify-self-end right-4 top-2.5"
aria-hidden="true"
focusable="false"
/>
</span>
</Localized>
}
)}
</div>
</Portal>
);

Просмотреть файл

@ -55,7 +55,7 @@ export const DialogMessage = ({
>
<CloseIcon
role="img"
className="close-icon"
className="w-4 h-4"
aria-hidden="true"
focusable="false"
/>

Просмотреть файл

@ -198,11 +198,6 @@ hr {
}
}
.close-icon {
height: 16px;
width: 16px;
}
#fxa-settings-content .subscription-management,
#main-content.card {
max-width: 640px;

Просмотреть файл

@ -68,18 +68,17 @@ import { CouponDetails } from 'fxa-shared/dto/auth/payments/coupon';
import { useParams } from 'react-router-dom';
const PaypalButton = React.lazy(() => import('../../components/PayPalButton'));
const ariaLabelledBy = "newsletter-error-alert-bar-header";
const NewsletterErrorAlertBar = () => {
return (
<AlertBar
className="alert newsletter-error"
className="newsletter-error"
dataTestId="newsletter-signup-error-message"
headerId={ariaLabelledBy}
headerId="newsletter-error-alert-bar-header"
localizedId="newsletter-signup-error"
>
Youre not signed up for product update emails. You can try again in
your account settings.
Youre not signed up for product update emails. You can try again in your
account settings.
</AlertBar>
);
};
@ -344,9 +343,12 @@ export const Checkout = ({
/>
<div
className={classNames("checkout-payment bg-white border-t-0 pt-4 px-4 pb-14 row-start-2 row-end-3 rounded-t-none rounded-b-lg shadow-sm shadow-grey-300 text-grey-600", {
hidden: transactionInProgress || subscriptionError,
})}
className={classNames(
'checkout-payment bg-white border-t-0 pt-4 px-4 pb-14 row-start-2 row-end-3 rounded-t-none rounded-b-lg shadow-sm shadow-grey-300 text-grey-600',
{
hidden: transactionInProgress || subscriptionError,
}
)}
data-testid="subscription-create"
>
<Localized id="new-user-step-1">
@ -467,9 +469,7 @@ export const Checkout = ({
</div>
</div>
{ (transactionInProgress && isMobile)
? null
: (
{transactionInProgress && isMobile ? null : (
<PlanDetails
{...{
selectedPlan,

Просмотреть файл

@ -105,7 +105,7 @@ export const ActionButton = ({
}
};
return <div className="action">{setActionButton()}</div>;
return <div className="inline">{setActionButton()}</div>;
};
export default ActionButton;

Просмотреть файл

@ -105,31 +105,31 @@ export const PaymentUpdateForm = ({
/>
);
const alertPendingAriaLabelledBy = "alert-pending-header";
const alertPendingAriaLabelledBy = 'alert-pending-header';
const alertErrorAriaLabelledBy = "alert-error-header";
const alertErrorAriaLabelledBy = 'alert-error-header';
const ariaLabelledBy = "error-invalid-billing-info-header";
const ariaDescribedBy = "error-invalid-billing-info-description";
const ariaLabelledBy = 'error-invalid-billing-info-header';
const ariaDescribedBy = 'error-invalid-billing-info-description';
const billingAgreementErrorAlertBarContent = () => (
<AlertBar
actionButton={actionButton}
className="alert alertError"
className="alert-error"
dataTestId="invalid-payment-error"
elems
headerId={alertErrorAriaLabelledBy}
localizedId="sub-route-missing-billing-agreement-payment-alert"
>
Invalid payment information; there is an error with your account.{' '}
{actionButton}
Invalid payment information; there is an error with your account.{' '}
{actionButton}
</AlertBar>
);
const fundingSourceErrorAlertBarContent = () => (
<AlertBar
actionButton={actionButton}
className="alert alertError"
className="alert-error"
dataTestId="invalid-payment-error-pending"
elems
headerId={alertErrorAriaLabelledBy}
@ -211,17 +211,20 @@ export const PaymentUpdateForm = ({
const onFormEngaged = useCallback(() => Amplitude.updatePaymentEngaged(), []);
// https://github.com/iamkun/dayjs/issues/639
const expirationDate = exp_month && exp_year && dayjs()
.set('month', Number(exp_month) - 1)
.set('year', Number(exp_year))
.format('MMMM YYYY');
const expirationDate =
exp_month &&
exp_year &&
dayjs()
.set('month', Number(exp_month) - 1)
.set('year', Number(exp_year))
.format('MMMM YYYY');
return (
<section className="settings-unit" aria-labelledby="payment-information">
<div className="payment-update" data-testid="payment-update">
{stripeSubmitInProgress && (
<AlertBar
className="alert alertPending"
className="alert-pending"
dataTestId="alert-pending"
headerId={alertPendingAriaLabelledBy}
localizedId="sub-route-idx-updating"
@ -230,9 +233,9 @@ export const PaymentUpdateForm = ({
</AlertBar>
)}
{!transactionInProgress && paypal_payment_error && (
getPaypalErrorAlertBarContent()
)}
{!transactionInProgress &&
paypal_payment_error &&
getPaypalErrorAlertBarContent()}
{transactionInProgress && (
<LoadingOverlay isLoading={transactionInProgress} />

Просмотреть файл

@ -184,18 +184,7 @@
// Wipe out button/link styles so that we can reuse the behavior from the
// manage/change button inside of the alert bar
// Alerts are going to be refactored
#top-bar .alert {
margin: 0;
}
.alert {
margin: 0;
.action {
display: inline;
}
.alert-error {
button {
background: inherit;
color: white;
@ -211,9 +200,6 @@
background: none;
}
}
.sr-only {
display: none;
}
a {
background: inherit;

Просмотреть файл

@ -258,7 +258,7 @@ export const Subscriptions = ({
{showPaymentSuccessAlert && (
<AlertBar
checked
className="alert alertSuccess alertCenter"
className="alert-success"
dataTestId="success-billing-update"
headerId="success-billing-update-header"
localizedId="sub-billing-update-success"

Просмотреть файл

@ -37,6 +37,13 @@ const Portal = ({ id, children }: PortalProps): React.ReactPortal | null => {
document.querySelectorAll(TOP_LEVEL_NONMODAL_DIVS_SELECTOR)
);
}
if (id === 'top-bar') {
el.setAttribute(
'class',
'portal fixed top-0 inset-x-0 p-0 z-[9999] tablet:px-16'
);
}
}
useEffect(() => {

Просмотреть файл

@ -2,14 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.portal#top-bar {
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 9999;
}
.portal#dialogs {
align-items: center;
bottom: 0;