зеркало из https://github.com/mozilla/fxa.git
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:
Родитель
b9fa47cf68
Коммит
ac0345f645
|
@ -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">
|
||||
|
||||
</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">
|
||||
|
||||
</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 {
|
||||
|
|
Загрузка…
Ссылка в новой задаче