Merge pull request #17056 from mozilla/FXA-9708

fix(payments-next): Implement a11y improvements
This commit is contained in:
Lisa Chan 2024-06-05 11:57:22 -04:00 коммит произвёл GitHub
Родитель 7798b2b631 f9f670fb2e
Коммит 18e5d172b0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
26 изменённых файлов: 453 добавлений и 267 удалений

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

@ -1,22 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
import { revalidatePath } from 'next/cache';
import { headers } from 'next/headers';
import { PaymentSection } from '@fxa/payments/ui';
import {
app,
getCartOrRedirectAction,
SupportedPages,
} from '@fxa/payments/ui/server';
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
import { auth, signIn } from 'apps/payments/next/auth';
import { headers } from 'next/headers';
import { CheckoutParams } from '../layout';
import {
getFakeCartData,
getContentfulContent,
} from 'apps/payments/next/app/_lib/apiClient';
import { PaymentSection } from '@fxa/payments/ui';
import { auth, signIn } from 'apps/payments/next/auth';
import { PrimaryButton } from 'libs/payments/ui/src/lib/client/components/PrimaryButton';
import { revalidatePath } from 'next/cache';
import { CheckoutParams } from '../layout';
export const dynamic = 'force-dynamic';
@ -62,13 +63,13 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
await signIn('fxa');
}}
>
<p className="text-grey-400 text-sm mt-2 mb-4">
<p className="text-grey-400 text-sm mt-2 pb-4 row-divider-grey-200">
{l10n.getFragmentWithSource(
'next-new-user-sign-in-link-2',
{
elems: {
a: (
<button className="underline text-grey-400 hover:text-grey-400">
<button className="underline hover:text-grey-400">
Sign in
</button>
),
@ -76,7 +77,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
},
<>
Already have a Mozilla account?&nbsp;
<button className="underline text-grey-400 hover:text-grey-400">
<button className="underline hover:text-grey-400">
Sign in
</button>
</>
@ -84,8 +85,6 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
</p>
</form>
<hr className="mx-auto w-full border-grey-200" />
<div className="p-6 text-center">
{/**
Temporary Content. This will be replaced in M3b by the Passwordless
@ -122,7 +121,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
{!session ? (
<h2
className="font-semibold text-grey-600 text-lg mt-14 mb-5"
className="font-semibold text-grey-600 text-lg mt-10 mb-5"
data-testid="header-prefix"
>
{l10n.getString(
@ -132,7 +131,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
</h2>
) : (
<h2
className="font-semibold text-grey-600 text-lg mt-14 mb-5"
className="font-semibold text-grey-600 text-lg mt-10 mb-5"
data-testid="header"
>
{l10n.getString(
@ -141,7 +140,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
)}
</h2>
)}
<h3 className="font-semibold my-3 text-grey-600 text-start">
<h3 className="font-semibold text-grey-600 text-start">
{l10n.getString(
'next-payment-method-first-approve',
`First you'll need to approve your subscription`

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

@ -21,7 +21,7 @@ export default function RootLayout({
<html lang="en">
<body>
<header
className="bg-white fixed flex justify-between items-center shadow h-16 left-0 top-0 mx-auto my-0 px-4 py-0 w-full z-10 tablet:h-20"
className="bg-white fixed flex justify-between items-center shadow h-16 left-0 top-0 mx-auto my-0 px-4 py-0 w-full z-40 tablet:h-20"
role="banner"
data-testid="header"
>

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

@ -5,7 +5,7 @@
// Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities
export * from './lib/utils/helpers';
export * from './lib/client/components/StripeWrapper';
export * from './lib/client/components/CheckoutForm';
export * from './lib/client/components/CheckoutCheckbox';
export * from './lib/client/components/PaymentSection';
export * from './lib/client/providers/Providers';

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

@ -3,9 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use client';
import * as HoverCard from '@radix-ui/react-hover-card';
import { useEffect, useState } from 'react';
import { Localized } from '@fluent/react';
import * as Tooltip from '@radix-ui/react-tooltip';
import { useEffect, useState } from 'react';
import { LinkExternal } from '@fxa/shared/react';
interface CheckoutCheckboxProps {
isRequired: boolean;
@ -38,84 +39,92 @@ export function CheckoutCheckbox({
};
return (
<HoverCard.Root open={isRequired && !isChecked}>
<label className="flex gap-5 items-center mt-6">
<HoverCard.Trigger>
<input
type="checkbox"
name="confirm"
className="grow-0 shrink-0 basis-4 scale-150 cursor-pointer"
checked={isChecked}
onChange={changeHandler}
/>
</HoverCard.Trigger>
{isClient && (
<Localized
id="next-payment-confirm-with-legal-links-static-3"
elems={{
termsOfServiceLink: (
<a
<Tooltip.Provider>
<Tooltip.Root open={isRequired && !isChecked}>
<label className="flex gap-5 items-center my-6">
<Tooltip.Trigger asChild>
<input
type="checkbox"
name="confirm"
className="grow-0 shrink-0 basis-4 scale-150 cursor-pointer"
checked={isChecked}
onChange={changeHandler}
required
aria-describedby="checkboxError"
aria-required
/>
</Tooltip.Trigger>
{isClient && (
<Localized
id="next-payment-confirm-with-legal-links-static-3"
elems={{
termsOfServiceLink: (
<LinkExternal
href={termsOfService}
className="text-blue-500 underline"
data-testid="link-external-terms-of-service"
>
Terms of Service
</LinkExternal>
),
privacyNoticeLink: (
<LinkExternal
href={privacyNotice}
className="text-blue-500 underline"
data-testid="link-external-privacy-notice"
>
Privacy Notice
</LinkExternal>
),
}}
>
<span className="font-normal text-sm leading-5 block">
I authorize Mozilla to charge my payment method for the amount
shown, according to{' '}
<LinkExternal
href={termsOfService}
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
data-testid="link-external-terms-of-service"
>
Terms of Service
</a>
),
privacyNoticeLink: (
<a
</LinkExternal>{' '}
and{' '}
<LinkExternal
href={privacyNotice}
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
data-testid="link-external-privacy-notice"
>
Privacy Notice
</a>
),
}}
>
<span className="font-normal text-sm block">
I authorize Mozilla to charge my payment method for the amount
shown, according to{' '}
<a
href={termsOfService}
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
>
Terms of Service
</a>{' '}
and{' '}
<a
href={privacyNotice}
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
>
Privacy Notice
</a>
, until I cancel my subscription.
</span>
</Localized>
)}
</label>
<HoverCard.Portal>
<HoverCard.Content
className="animate-slide-up z-20"
sideOffset={20}
align="start"
alignOffset={50}
arrowPadding={20}
>
<Localized id="next-payment-confirm-checkbox-error">
<div className="text-white text-sm bg-alert-red py-1.5 px-4">
You need to complete this before moving forward
</div>
</Localized>
<HoverCard.Arrow className="fill-alert-red" height={11} width={22} />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
</LinkExternal>
, until I cancel my subscription.
</span>
</Localized>
)}
<Tooltip.Portal>
<Tooltip.Content
id="checkboxError"
className="animate-slide-down z-20"
side="bottom"
sideOffset={20}
align="start"
alignOffset={50}
arrowPadding={20}
role="alert"
>
<Localized id="next-payment-confirm-checkbox-error">
<div className="text-white text-sm bg-alert-red py-1.5 px-4">
You need to complete this before moving forward
</div>
</Localized>
<Tooltip.Arrow
className="fill-alert-red"
height={11}
width={22}
/>
</Tooltip.Content>
</Tooltip.Portal>
</label>
</Tooltip.Root>
</Tooltip.Provider>
);
}

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

@ -1,8 +1,8 @@
## Checkout Form
next-new-user-submit = Subscribe Now
next-payment-validate-name-error = Please enter your full name
# Label for the Full Name input
payment-name-label = Name as it appears on your card
payment-name-placeholder = Full Name

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

@ -3,24 +3,28 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use client';
import { Localized, useLocalization } from '@fluent/react';
import * as Form from '@radix-ui/react-form';
import {
PaymentElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
import { StripePaymentElementChangeEvent } from '@stripe/stripe-js';
import { checkoutCartWithStripe } from '../../actions/checkoutCartWithStripe';
import { useEffect, useState } from 'react';
import { handleStripeErrorAction } from '../../actions/handleStripeError';
import LockImage from '@fxa/shared/assets/images/lock.svg';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import * as Form from '@radix-ui/react-form';
import { Localized, useLocalization } from '@fluent/react';
import { useEffect, useState } from 'react';
import LockImage from '@fxa/shared/assets/images/lock.svg';
import { CheckoutCheckbox } from './CheckoutCheckbox';
import { PrimaryButton } from './PrimaryButton';
import { checkoutCartWithStripe } from '../../actions/checkoutCartWithStripe';
import { handleStripeErrorAction } from '../../actions/handleStripeError';
interface CheckoutFormProps {
readOnly: boolean;
cmsCommonContent: {
termsOfServiceUrl: string;
privacyNoticeUrl: string;
};
cart: {
id: string;
version: number;
@ -29,11 +33,18 @@ interface CheckoutFormProps {
locale: string;
}
export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
const router = useRouter();
const stripe = useStripe();
export function CheckoutForm({
cmsCommonContent,
cart,
locale,
}: CheckoutFormProps) {
const { l10n } = useLocalization();
const elements = useElements();
const router = useRouter();
const stripe = useStripe();
const [formEnabled, setFormEnabled] = useState(false);
const [showConsentError, setShowConsentError] = useState(false);
const [isPaymentElementLoading, setIsPaymentElementLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [stripeFieldsComplete, setStripeFieldsComplete] = useState(false);
@ -68,7 +79,7 @@ export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
) => {
event.preventDefault();
if (!stripe || !elements || readOnly) {
if (!stripe || !elements) {
// Stripe.js hasn't yet loaded.
// Make sure to disable form submission until Stripe.js has loaded.
return;
@ -135,71 +146,96 @@ export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
const nonStripeFieldsComplete = !!fullName;
return (
<Form.Root className="flex flex-col gap-4" onSubmit={submitHandler}>
{!isPaymentElementLoading && (
<Form.Field name="name" serverInvalid={hasFullNameError}>
<Form.Label className="font-medium text-sm text-grey-400 block mb-1 text-start">
<Localized id="payment-name-label">
Name as it appears on your card
</Localized>
</Form.Label>
<Form.Control asChild>
<input
className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
type="text"
data-testid="name"
placeholder={l10n.getString(
'payment-name-placeholder',
{},
'Full Name'
)}
readOnly={readOnly}
value={fullName}
onChange={(e) => {
setFullName(e.target.value);
setHasFullNameError(!e.target.value);
}}
/>
</Form.Control>
{hasFullNameError && (
<Form.Message asChild>
<p className="text-sm mt-1 text-alert-red">
Please enter your name
</p>
</Form.Message>
)}
</Form.Field>
)}
<PaymentElement
options={{
layout: {
type: 'accordion',
defaultCollapsed: false,
radios: false,
spacedAccordionItems: true,
},
readOnly,
<Form.Root onSubmit={submitHandler}>
<CheckoutCheckbox
isRequired={showConsentError}
termsOfService={cmsCommonContent.termsOfServiceUrl}
privacyNotice={cmsCommonContent.privacyNoticeUrl}
notifyCheckboxChange={(consentCheckbox) => {
setFormEnabled(consentCheckbox);
setShowConsentError(true);
}}
/>
{!isPaymentElementLoading && (
<Form.Submit asChild>
<Localized id="next-new-user-submit">
<div
className={
formEnabled
? 'mt-10'
: 'mt-10 cursor-not-allowed relative focus:border-blue-400 focus:outline-none focus:shadow-input-blue-focus after:absolute after:content-[""] after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:opacity-50 after:z-10'
}
onClick={() => setShowConsentError(true)}
>
<Localized id="next-new-user-card-title">
<h3 className="font-semibold text-grey-600 text-start">
Enter your card information
</h3>
</Localized>
{!isPaymentElementLoading && (
<Form.Field
name="name"
serverInvalid={hasFullNameError}
className="my-6"
>
<Form.Label className="text-grey-400 block mb-1 text-start">
<Localized id="payment-name-label">
Name as it appears on your card
</Localized>
</Form.Label>
<Form.Control asChild>
<input
className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
type="text"
data-testid="name"
placeholder={l10n.getString(
'payment-name-placeholder',
{},
'Full Name'
)}
readOnly={!formEnabled}
tabIndex={formEnabled ? 0 : -1}
value={fullName}
onChange={(e) => {
setFullName(e.target.value);
setHasFullNameError(!e.target.value);
}}
aria-required
/>
</Form.Control>
{hasFullNameError && (
<Form.Message asChild>
<Localized id="next-payment-validate-name-error">
<p className="mt-1 text-alert-red" role="alert">
Please enter your name
</p>
</Localized>
</Form.Message>
)}
</Form.Field>
)}
<PaymentElement
options={{
layout: {
type: 'accordion',
defaultCollapsed: false,
radios: false,
spacedAccordionItems: true,
},
readOnly: !formEnabled,
}}
/>
{!isPaymentElementLoading && (
<Form.Submit asChild>
<PrimaryButton
type="submit"
aria-disabled={
!stripeFieldsComplete || !nonStripeFieldsComplete || loading
}
>
<Image
src={LockImage}
className="h-4 w-4 my-0 mx-3 relative top-0.5"
alt=""
/>
Subscribe Now
<Image src={LockImage} className="h-4 w-4 mx-3" alt="" />
<Localized id="next-new-user-submit">Subscribe Now</Localized>
</PrimaryButton>
</Localized>
</Form.Submit>
)}
</Form.Submit>
)}
</div>
</Form.Root>
);
}

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

@ -1,12 +1,11 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
'use client';
import { useState } from 'react';
import { CheckoutCheckbox } from './CheckoutCheckbox';
import { CheckoutForm } from './CheckoutForm';
import { StripeWrapper } from './StripeWrapper';
import { Localized } from '@fluent/react';
interface PaymentFormProps {
cmsCommonContent: {
@ -31,42 +30,16 @@ export function PaymentSection({
cart,
locale,
}: PaymentFormProps) {
const [formEnabled, setFormEnabled] = useState(false);
const [showConsentError, setShowConsentError] = useState(false);
return (
<>
<CheckoutCheckbox
isRequired={showConsentError}
termsOfService={cmsCommonContent.termsOfServiceUrl}
privacyNotice={cmsCommonContent.privacyNoticeUrl}
notifyCheckboxChange={(consentCheckbox) => {
setFormEnabled(consentCheckbox);
setShowConsentError(true);
}}
<StripeWrapper
amount={paymentsInfo.amount}
currency={paymentsInfo.currency}
>
<CheckoutForm
cmsCommonContent={cmsCommonContent}
cart={cart}
locale={locale}
/>
<div
className={
formEnabled
? 'mt-14'
: 'mt-14 relative cursor-not-allowed focus:border-blue-400 focus:outline-none focus:shadow-input-blue-focus after:absolute after:content-[""] after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:opacity-50 after:z-10'
}
aria-disabled={!formEnabled}
onClick={() => setShowConsentError(true)}
>
<Localized id="next-new-user-card-title">
<h3 className="font-semibold text-grey-600 text-start mt-3 mb-6">
Enter your card information
</h3>
</Localized>
<StripeWrapper
readOnly={!formEnabled}
amount={paymentsInfo.amount}
currency={paymentsInfo.currency}
cart={cart}
locale={locale}
/>
</div>
</>
</StripeWrapper>
);
}

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

@ -10,7 +10,7 @@ export function PrimaryButton({ children, ...props }: PrimaryButtonProps) {
return (
<button
{...props}
className={`flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4 mt-6 hover:bg-blue-700 aria-disabled:relative aria-disabled:after:absolute aria-disabled:after:content-[''] aria-disabled:after:top-0 aria-disabled:after:left-0 aria-disabled:after:w-full aria-disabled:after:h-full aria-disabled:after:bg-white aria-disabled:after:opacity-50 aria-disabled:after:z-30 aria-disabled:border-none ${props.className}`}
className={`flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4 mt-6 hover:bg-blue-700 z-10 aria-disabled:relative aria-disabled:after:absolute aria-disabled:after:content-[''] aria-disabled:after:top-0 aria-disabled:after:left-0 aria-disabled:after:w-full aria-disabled:after:h-full aria-disabled:after:bg-white aria-disabled:after:opacity-50 aria-disabled:after:z-30 aria-disabled:border-none ${props.className}`}
>
{children}
</button>

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

@ -3,30 +3,21 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use client';
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { CheckoutForm } from './CheckoutForm';
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
import { useContext, useState } from 'react';
import { ConfigContext } from '../providers/ConfigProvider';
interface StripeWrapperProps {
readOnly: boolean;
amount: number;
currency: string;
cart: {
id: string;
version: number;
email: string | null;
};
locale: string;
children: React.ReactNode;
}
export function StripeWrapper({
readOnly,
amount,
currency,
cart,
locale,
children,
}: StripeWrapperProps) {
const config = useContext(ConfigContext);
const [stripePromise] = useState(() => loadStripe(config.stripePublicApiKey));
@ -42,6 +33,7 @@ export function StripeWrapper({
fontFamily:
'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
fontSizeBase: '16px',
fontSizeSm: '16px',
fontWeightNormal: '500',
colorDanger: '#D70022',
},
@ -67,7 +59,7 @@ export function StripeWrapper({
return (
<Elements stripe={stripePromise} options={options}>
<CheckoutForm readOnly={readOnly} cart={cart} locale={locale} />
{children}
</Elements>
);
}

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

@ -2,10 +2,10 @@
* 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/. */
import { Invoice } from '@fxa/payments/cart';
import Image from 'next/image';
import { formatPlanPricing } from '../utils/helpers';
import { Invoice } from '@fxa/payments/cart';
import { LocalizerRsc } from '@fxa/shared/l10n/server';
import { formatPlanPricing } from '../utils/helpers';
type ListLabelItemProps = {
labelLocalizationId: string;
@ -114,7 +114,7 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) {
))}
</ul>
<ul className="row-divider-grey-200 py-6">
<ul className="pt-6">
{!!listAmount && (
<ListLabelItem
{...{
@ -165,26 +165,26 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) {
key={taxRate.title}
/>
))}
</ul>
<div className="plan-details-item pt-4 pb-6 font-semibold">
<h3 className="text-base">
{l10n.getString('next-plan-details-total-label', 'Total')}
</h3>
<span
className="overflow-hidden text-ellipsis text-lg whitespace-nowrap"
data-testid="total-price"
id="total-price"
>
{l10n.getString(
`plan-price-interval-${interval}`,
{
amount: l10n.getLocalizedCurrency(totalAmount, currency),
},
formatPlanPricing(totalAmount, currency, interval)
)}
</span>
</div>
<li className="plan-details-item mt-6 pt-4 pb-6 font-semibold border-t border-grey-200">
<h3 className="text-base">
{l10n.getString('next-plan-details-total-label', 'Total')}
</h3>
<span
className="overflow-hidden text-ellipsis text-lg whitespace-nowrap"
data-testid="total-price"
id="total-price"
>
{l10n.getString(
`plan-price-interval-${interval}`,
{
amount: l10n.getLocalizedCurrency(totalAmount, currency),
},
formatPlanPricing(totalAmount, currency, interval)
)}
</span>
</li>
</ul>
{/* TODO - Add InfoBox as part of Coupon Form - Consider adding as child component */}
</div>
);

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

@ -3,9 +3,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Image from 'next/image';
import checkLogo from '@fxa/shared/assets/images/check.svg';
import { CartState } from '@fxa/shared/db/mysql/account';
import { LocalizerRsc } from '@fxa/shared/l10n/server';
import checkLogo from '@fxa/shared/assets/images/check.svg';
const getComponentTitle = (cartState: CartState) => {
switch (cartState) {
@ -53,7 +53,10 @@ export async function SubscriptionTitle({
const displaySubtitle = subheaders.includes(cartState);
return (
<header className="page-title-container">
<header
className="page-title-container"
aria-label={l10n.getString(componentTitle.titleFtl, componentTitle.title)}
>
<h1 className="page-header">
{l10n.getString(componentTitle.titleFtl, componentTitle.title)}
</h1>

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

@ -84,15 +84,15 @@ module.exports = {
'0%': { transform: 'rotate(0)' },
'100%': { transform: 'rotate(360deg)' },
},
'slide-up': {
'0%': { opacity: 0, transform: 'translateY(10px)' },
'slide-down': {
'0%': { opacity: 0, transform: 'translateY(-10px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
},
},
animation: {
'delayed-fade-in': 'fade-in 1s linear 5s forwards',
spin: 'rotate 0.8s linear infinite',
'slide-up': 'slide-up 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
'slide-down': 'slide-down 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
},
listStyleType: {
circle: 'circle',
@ -275,7 +275,7 @@ module.exports = {
},
},
plugins: [
plugin(function({ addUtilities }) {
plugin(function ({ addUtilities }) {
const customUtilities = {
'.clip-auto': {
clip: 'auto',
@ -284,7 +284,7 @@ module.exports = {
addUtilities(customUtilities, ['responsive', 'hover', 'focus']);
}),
plugin(function({ addComponents }) {
plugin(function ({ addComponents }) {
const carets = {
'.caret-top': {
borderLeft: '0.75rem solid transparent',

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

@ -0,0 +1,18 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

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

@ -0,0 +1,11 @@
# shared-react
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build shared-react` to build the library.
## Running unit tests
Run `nx test shared-react` to execute the unit tests via [Jest](https://jestjs.io).

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

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'shared-react',
preset: '../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/shared/react',
};

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

@ -0,0 +1,4 @@
{
"name": "@fxa/shared/react",
"version": "0.0.1"
}

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

@ -0,0 +1,27 @@
{
"name": "shared-react",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/react/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/shared/react",
"tsConfig": "libs/shared/react/tsconfig.lib.json",
"packageJson": "libs/shared/react/package.json",
"main": "libs/shared/react/src/index.ts",
"assets": ["libs/shared/react/*.md"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/react/jest.config.ts"
}
}
}
}

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

@ -0,0 +1,4 @@
## Strings shared between multiple FxA products for external link
# Message for screen readers, to announce that external link will open in new window
next-link-sr-new-window = Opens in new window

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

@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
'use client';
import { Localized } from '@fluent/react';
interface LinkExternalProps {
className?: string;
href: string;
children: React.ReactNode;
title?: string;
'data-testid'?: string;
rel?: 'noopener noreferrer' | 'author';
tabIndex?: number;
onClick?: () => void;
}
export const LinkExternal = ({
className,
href,
children,
title,
'data-testid': testid = 'link-external',
rel = 'noopener noreferrer',
tabIndex,
onClick,
}: LinkExternalProps) => (
<a
data-testid={testid}
target="_blank"
{...{
className,
href,
title,
rel,
tabIndex,
onClick,
}}
>
{children}
<Localized id="next-link-sr-new-window">
<span className="sr-only">Opens in new window</span>
</Localized>
</a>
);
export default LinkExternal;

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

@ -0,0 +1,5 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
export * from './components/LinkExternal';

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

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

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

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts", "src/**/*.tsx"]
}

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

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

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

@ -72,7 +72,7 @@
"@opentelemetry/sdk-trace-node": "^1.23.0",
"@opentelemetry/sdk-trace-web": "^1.23.0",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@sentry/browser": "^7.113.0",
"@sentry/integrations": "^7.113.0",
"@sentry/node": "^7.113.0",

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

@ -58,6 +58,7 @@
"@fxa/shared/notifier": ["libs/shared/notifier/src/index.ts"],
"@fxa/shared/otp": ["libs/shared/otp/src/index.ts"],
"@fxa/shared/pem-jwk": ["libs/shared/pem-jwk/src/index.ts"],
"@fxa/shared/react": ["libs/shared/react/src/index.ts"],
"@fxa/shared/sentry": ["libs/shared/sentry/src/index.ts"],
"@fxa/vendored/common-password-list": [
"libs/vendored/common-password-list/src/index.ts"

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

@ -14964,34 +14964,6 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-hover-card@npm:^1.0.7":
version: 1.0.7
resolution: "@radix-ui/react-hover-card@npm:1.0.7"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-dismissable-layer": 1.0.5
"@radix-ui/react-popper": 1.1.3
"@radix-ui/react-portal": 1.0.4
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-controllable-state": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 812c348d8331348774b0460cd9058fdb34e0a4e167cc3ab7350d60d0ac374c673e8159573919da299f58860b8eeb9d43c21ccb679cf6db70f5db0386359871ef
languageName: node
linkType: hard
"@radix-ui/react-id@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-id@npm:1.0.1"
@ -15345,6 +15317,37 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-tooltip@npm:^1.0.7":
version: 1.0.7
resolution: "@radix-ui/react-tooltip@npm:1.0.7"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-dismissable-layer": 1.0.5
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-popper": 1.1.3
"@radix-ui/react-portal": 1.0.4
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-slot": 1.0.2
"@radix-ui/react-use-controllable-state": 1.0.1
"@radix-ui/react-visually-hidden": 1.0.3
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 894d448c69a3e4d7626759f9f6c7997018fe8ef9cde098393bd83e10743d493dfd284eef041e46accc45486d5a5cd5f76d97f56afbdace7aed6e0cb14007bf15
languageName: node
linkType: hard
"@radix-ui/react-use-callback-ref@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1"
@ -38801,7 +38804,7 @@ fsevents@~2.1.1:
"@opentelemetry/sdk-trace-node": ^1.23.0
"@opentelemetry/sdk-trace-web": ^1.23.0
"@radix-ui/react-form": ^0.0.3
"@radix-ui/react-hover-card": ^1.0.7
"@radix-ui/react-tooltip": ^1.0.7
"@sentry/browser": ^7.113.0
"@sentry/integrations": ^7.113.0
"@sentry/node": ^7.113.0