refactor(payments): payment form tailwind convert

Because:

- We want to convert the PaymentForm to using Tailwind classes

This commit:

- Replaces all existing classes with Tailwind classes
- Updates fxa-react LoadingSpinner component to support multiple images

Closes #fxa-5633
This commit is contained in:
Reino Muhl 2022-11-04 18:22:40 -04:00
Родитель b9fa47cf68
Коммит ac0345f645
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: F5AE84225E04F335
11 изменённых файлов: 181 добавлений и 118 удалений

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

@ -29,6 +29,9 @@ brand-name-firefox-logo = { -brand-name-firefox } logo
close-aria =
.aria-label = Close modal
# Aria label for spinner image indicating data is loading
app-loading-spinner-aria-label-loading = Loading...
## App error dialog
general-error-heading = General application error

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

@ -1,28 +1,6 @@
@import '../../../../fxa-content-server/app/styles/breakpoints';
form.payment {
.pricing-and-saved-payment {
display: flex;
font-size: 15px;
justify-content: space-between;
.pricing {
display: flex;
align-items: center;
}
.c-card {
margin: 0;
}
}
hr {
margin: 24px 0 0;
@include respond-to('simpleSmall') {
margin-top: 16px;
}
}
.input-row {
margin: 24px 0 0;
@ -60,21 +38,6 @@ form.payment {
line-height: 45px;
}
}
.button-row button {
margin-top: 32px;
}
.lock::before {
background-image: url('./images/lock.svg');
background-position: 0 2px;
background-repeat: no-repeat;
content: '\00a0';
display: inline-block;
height: 18px;
margin: 0 8px;
width: 14px;
}
}
@include respond-to('simpleSmall') {
@ -82,12 +45,4 @@ form.payment {
form.payment .input-row input::-moz-placeholder {
line-height: 38px !important;
}
.main-content .payments-card {
width: 100%;
.input-row-group {
padding-bottom: 8px;
}
}
}

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

@ -1,5 +1,11 @@
import React from 'react';
import { render, cleanup, act, fireEvent } from '@testing-library/react';
import {
render,
cleanup,
act,
fireEvent,
queryByTestId,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import waitForExpect from 'wait-for-expect';
@ -68,6 +74,7 @@ it('renders all expected default fields and elements', () => {
expect(container.querySelector('button.cancel')).not.toBeInTheDocument();
expect(container.querySelector('span.spinner')).not.toBeInTheDocument();
expect(queryByTestId(container, 'loading-spinner')).not.toBeInTheDocument();
expect(getByTestId('submit')).toHaveAttribute('disabled');
for (let testid of ['name', 'cardElement']) {
@ -186,7 +193,7 @@ it('renders a progress spinner when submitted, disables further submission (issu
await waitForExpect(() => expect(onSubmit).toHaveBeenCalled());
expect(queryByTestId('spinner-submit')).toBeInTheDocument();
expect(queryByTestId('loading-spinner')).toBeInTheDocument();
expect(getByTestId('submit')).toHaveAttribute('disabled');
fireEvent.submit(getByTestId('paymentForm'));
@ -197,7 +204,7 @@ it('renders a progress spinner when submitted, disables further submission (issu
it('renders a progress spinner when inProgress = true', () => {
const { queryByTestId } = render(<Subject {...{ inProgress: true }} />);
expect(queryByTestId('spinner-submit')).toBeInTheDocument();
expect(queryByTestId('loading-spinner')).toBeInTheDocument();
});
it('renders a progress spinner when inProgress = true and onCancel supplied', () => {
@ -205,7 +212,7 @@ it('renders a progress spinner when inProgress = true and onCancel supplied', ()
const { queryByTestId } = render(
<Subject {...{ inProgress: true, onCancel }} />
);
expect(queryByTestId('spinner-update')).toBeInTheDocument();
expect(queryByTestId('loading-spinner')).toBeInTheDocument();
});
it('includes the cancel button when onCancel supplied', () => {

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

@ -42,6 +42,10 @@ import {
} from '../../lib/PaymentProvider';
import { PaymentProviderDetails } from '../PaymentProviderDetails';
import { PaymentConsentCheckbox } from '../PaymentConsentCheckbox';
import LoadingSpinner, {
SpinnerType,
} from 'fxa-react/components/LoadingSpinner';
import LockImage from './images/lock.svg';
export type StripePaymentSubmitResult = {
stripe: Stripe;
@ -194,7 +198,7 @@ export const PaymentForm = ({
const paymentSource =
plan && isExistingCustomer(customer) ? (
<div className="pricing-and-saved-payment">
<div className="flex items-center justify-between text-base">
<Localized
id={`plan-price-${plan.interval}`}
vars={{
@ -202,7 +206,7 @@ export const PaymentForm = ({
intervalCount: plan.interval_count!,
}}
>
<div className="pricing"></div>
<p></p>
</Localized>
<PaymentProviderDetails customer={customer!} />
</div>
@ -238,11 +242,11 @@ export const PaymentForm = ({
);
const buttons = onCancel ? (
<div className="button-row">
<div className="mt-8 mb-5 flex gap-4 mobileLandscape:gap-6">
<Localized id="payment-cancel-btn">
<button
data-testid="cancel"
className="button settings-button cancel secondary-button"
className="payment-button h-10 border-0 bg-grey-900/10 mobileLandscape:h-12"
onClick={onCancel}
>
Cancel
@ -254,14 +258,15 @@ export const PaymentForm = ({
>
<SubmitButton
data-testid="submit"
className="button settings-button primary-button"
className="payment-button cta-primary h-10 mobileLandscape:h-12"
name="submit"
disabled={inProgress}
>
{inProgress ? (
<span data-testid="spinner-update" className="spinner">
&nbsp;
</span>
<LoadingSpinner
spinnerType={SpinnerType.White}
imageClassName="w-8 h-8 animate-spin"
/>
) : submitButtonCopy ? (
<span>{submitButtonCopy}</span>
) : (
@ -271,28 +276,36 @@ export const PaymentForm = ({
</Localized>
</div>
) : (
<div className="button-row">
<div className="mb-5">
<Localized id="payment-submit-btn">
<SubmitButton
data-testid="submit"
className="button"
className="payment-button cta-primary !font-bold w-full mt-8 h-12"
name="submit"
disabled={!allowSubmit}
>
{showProgressSpinner ? (
<span data-testid="spinner-submit" className="spinner">
&nbsp;
</span>
<LoadingSpinner
spinnerType={SpinnerType.White}
imageClassName="w-8 h-8 animate-spin"
/>
) : (
<Localized
id={
submitButtonL10nId
? submitButtonL10nId
: payButtonL10nId(customer)
}
>
<span className="lock">{submitButtonCopy}</span>
</Localized>
<div className="text-center">
<img
src={LockImage}
className="h-4 w-4 my-0 mx-3 relative top-0.5"
alt=""
/>
<Localized
id={
submitButtonL10nId
? submitButtonL10nId
: payButtonL10nId(customer)
}
>
<span>{submitButtonCopy}</span>
</Localized>
</div>
)}
</SubmitButton>
</Localized>
@ -308,14 +321,12 @@ export const PaymentForm = ({
{...{ onChange }}
>
{paymentSource}
{confirm && plan && (
<>
<PaymentConsentCheckbox plan={plan} />
<hr />
<hr className="mt-4 tablet:mt-6" />
</>
)}
{buttons}
</Form>
);

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

@ -292,6 +292,19 @@ export const STRIPE_ELEMENT_STYLES = {
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
fontSize: '16px',
fontWeight: '500',
'::placeholder': {
color: '#767676',
fontSize: '16px',
fontWeight: '400',
height: '45px',
lineHeight: 'normal',
},
'::-moz-placeholder': {
lineHeight: '45px',
},
'&:-moz-ui-invalid': {
boxShadow: 'none',
},
},
invalid: {
color: '#0c0c0d',

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

@ -125,3 +125,7 @@
@apply tablet:grid-cols-[minmax(min-content,500px)_minmax(20rem,1fr)] tablet:gap-x-8 tablet:mt-[80px] tablet:mb-auto tablet:mx-3;
@apply desktop:grid-cols-[600px_1fr];
}
.payment-button {
@apply cta-base-p flex-1 font-semibold rounded-md text-base;
}

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

@ -1,10 +1,36 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { LoadingSpinner } from './index';
import { LoadingSpinner, SpinnerType } from './index';
it('renders as expected', () => {
it('renders defaults as expected', () => {
render(<LoadingSpinner />);
const result = screen.queryByTestId('loading-spinner');
const spinnerType = screen.queryByTestId('loading-spinner-blue');
expect(result).toBeInTheDocument();
expect(spinnerType).toBeInTheDocument();
});
it('renders with custom spinner classNames', () => {
render(<LoadingSpinner imageClassName="testclass" />);
const result = screen.queryByTestId('loading-spinner');
const spinnerImage = screen.queryByTestId('loading-spinner-blue');
expect(result).toBeInTheDocument();
expect(spinnerImage).toHaveClass('testclass');
});
it('renders blue spinner as expected', () => {
render(<LoadingSpinner spinnerType={SpinnerType.Blue} />);
const result = screen.queryByTestId('loading-spinner');
const spinnerType = screen.queryByTestId('loading-spinner-blue');
expect(result).toBeInTheDocument();
expect(spinnerType).toBeInTheDocument();
});
it('renders blue spinner as expected', () => {
render(<LoadingSpinner spinnerType={SpinnerType.White} />);
const result = screen.queryByTestId('loading-spinner');
const spinnerType = screen.queryByTestId('loading-spinner-white');
expect(result).toBeInTheDocument();
expect(spinnerType).toBeInTheDocument();
});

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

@ -1,14 +1,57 @@
import { useLocalization } from '@fluent/react';
import React from 'react';
import { ReactComponent as Spinner } from './spinner.svg';
import { ReactComponent as SpinnerWhite } from './spinnerwhite.svg';
export const LoadingSpinner = ({ className }: { className?: string }) => (
<div {...{ className }} data-testid="loading-spinner">
<Spinner
className="w-10 h-10 animate-spin"
role="img"
aria-label="Loading..."
/>
</div>
);
export enum SpinnerType {
Blue,
White,
}
export const LoadingSpinner = ({
className,
imageClassName = 'w-10 h-10 animate-spin',
spinnerType = SpinnerType.Blue,
}: {
className?: string;
imageClassName?: string;
spinnerType?: SpinnerType;
}) => {
const { l10n } = useLocalization();
const loadingAriaLabel = l10n.getString(
'app-loading-spinner-aria-label-loading',
null,
'Loading...'
);
let spinnerImage;
switch (spinnerType) {
case SpinnerType.White:
spinnerImage = (
<SpinnerWhite
className={imageClassName}
role="img"
aria-label={loadingAriaLabel}
data-testid="loading-spinner-white"
/>
);
break;
case SpinnerType.Blue:
default:
spinnerImage = (
<Spinner
className={imageClassName}
role="img"
aria-label={loadingAriaLabel}
data-testid="loading-spinner-blue"
/>
);
}
return (
<div {...{ className }} data-testid="loading-spinner">
{spinnerImage}
</div>
);
};
export default LoadingSpinner;

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

@ -0,0 +1 @@
<svg viewBox="0 0 73 73" width="73" height="73" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient x1="93.093%" y1="52.773%" x2="68.513%" y2="119.326%" id="a"><stop stop-color="#FFF" stop-opacity="0" offset="0%"/><stop stop-color="#FFF" offset="69.37%"/><stop stop-color="#FFF" offset="100%"/><stop stop-color="#FFF" stop-opacity=".005" offset="100%"/><stop stop-color="#FFF" stop-opacity="0" offset="100%"/><stop stop-color="#FFF" stop-opacity="0" offset="100%"/></linearGradient><path id="b" d="M0 0h48v60H0z"/></defs><g transform="translate(-5 -1)" fill="none" fill-rule="evenodd"><path d="M41.8 73.8c-19.9 0-36-16.1-36-36 0-19.7 15.8-35.6 35.3-36h.7c2.8.4 5 2.7 5 5.5s-2.2 5.2-5 5.4c-13.8.1-25 11.3-25 25.1s11.2 25 25 25 25-11.2 25-25h11c0 19.9-16.1 36-36 36z" fill="url(#a)"/><mask id="c" fill="#fff"><use xlink:href="#b"/></mask><path d="M41.8 73.8c-19.9 0-36-16.1-36-36 0-19.7 15.8-35.6 35.3-36h.7c2.8.4 5 2.7 5 5.5s-2.2 5.2-5 5.4c-13.8.1-25 11.3-25 25.1s11.2 25 25 25 25-11.2 25-25h11c0 19.9-16.1 36-36 36z" fill="#FFF" mask="url(#c)"/></g></svg>

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

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

@ -4,10 +4,10 @@
.transition-standard {
@apply transition duration-150;
}
&:active {
@apply transition duration-150;
}
.transition-standard:active {
@apply transition duration-150;
}
@media (hover: hover) {

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

@ -15,53 +15,53 @@
.cta-neutral {
@apply bg-grey-10 border-grey-200;
}
&:active {
@apply border-grey-400 bg-grey-100 text-grey-400;
}
.cta-neutral:active {
@apply border-grey-400 bg-grey-100 text-grey-400;
}
&:focus,
&:focus-visible {
@apply bg-white outline outline-2 outline-offset-2 outline-blue-500;
}
.cta-neutral:focus,
.cta-neutral:focus-visible {
@apply bg-white outline outline-2 outline-offset-2 outline-blue-500;
}
&:disabled {
@apply bg-white text-grey-300 border-grey-200 cursor-not-allowed;
}
.cta-neutral:disabled {
@apply bg-white text-grey-300 border-grey-200 cursor-not-allowed;
}
.cta-primary {
@apply bg-blue-500 border-blue-600 text-white;
}
&:active {
@apply bg-blue-800 border-blue-800;
}
.cta-primary:active {
@apply bg-blue-800 border-blue-800;
}
&:focus,
&:focus-visible {
@apply outline outline-2 outline-offset-2 outline-blue-500;
}
.cta-primary:focus,
.cta-primary:focus-visible {
@apply outline outline-2 outline-offset-2 outline-blue-500;
}
&:disabled {
@apply bg-blue-500/40 border-transparent text-white/50 cursor-not-allowed;
}
.cta-primary:disabled {
@apply bg-blue-500/40 border-transparent text-white/50 cursor-not-allowed;
}
.cta-caution {
@apply bg-red-500 border-red-600 text-white;
}
&:active {
@apply bg-red-800 border-red-800;
}
.cta-caution:active {
@apply bg-red-800 border-red-800;
}
&:focus,
&:focus-visible {
@apply border-transparent outline-dotted outline-black;
}
.cta-caution:focus,
.cta-caution:focus-visible {
@apply border-transparent outline-dotted outline-black;
}
&:disabled {
@apply bg-red-500/40 border-transparent text-white/50 cursor-not-allowed;
}
.cta-caution:disabled {
@apply bg-red-500/40 border-transparent text-white/50 cursor-not-allowed;
}
.cta-base {