[WEB-4197] chore: auth forms semantics and accessibility #7128

This commit is contained in:
Aaryan Khandelwal 2025-05-30 18:22:20 +05:30 committed by GitHub
parent 01b685ea57
commit cb92108bf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 252 additions and 75 deletions

View file

@ -1,5 +1,7 @@
import { FC } from "react";
import { Info, X } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// helpers
import { TAuthErrorInfo } from "@/helpers/authentication.helper";
@ -10,20 +12,28 @@ type TAuthBanner = {
export const AuthBanner: FC<TAuthBanner> = (props) => {
const { bannerData, handleBannerData } = props;
// translation
const { t } = useTranslation();
if (!bannerData) return <></>;
return (
<div className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
<div
role="alert"
className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10"
>
<div className="size-4 flex-shrink-0 grid place-items-center">
<Info size={16} className="text-custom-primary-100" />
</div>
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div>
<div
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
onClick={() => handleBannerData && handleBannerData(undefined)}
<p className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</p>
<button
type="button"
className="relative ml-auto size-6 rounded-sm grid place-items-center transition-all hover:bg-custom-primary-100/20 text-custom-primary-100/80"
onClick={() => handleBannerData?.(undefined)}
aria-label={t("aria_labels.auth_forms.close_alert")}
>
<X className="w-4 h-4 flex-shrink-0" />
</div>
<X className="size-4" />
</button>
</div>
);
};

View file

@ -102,9 +102,9 @@ export const AuthHeader: FC<TAuthHeader> = observer((props) => {
return (
<>
<div className="space-y-1 text-center">
<h3 className="text-3xl font-bold text-onboarding-text-100">
<h1 className="text-3xl font-bold text-onboarding-text-100">
{typeof header === "string" ? t(header) : header}
</h3>
</h1>
<p className="font-medium text-onboarding-text-400">{t(subHeader)}</p>
</div>
{children}

View file

@ -47,7 +47,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
return (
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4">
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
<label htmlFor="email" className="text-sm text-onboarding-text-300 font-medium">
{t("auth.common.email.label")}
</label>
<div
@ -76,13 +76,17 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
ref={inputRef}
/>
{email.length > 0 && (
<XCircle
className="h-[46px] w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs"
<button
type="button"
onClick={() => {
setEmail("");
inputRef.current?.focus();
}}
/>
className="absolute right-3 size-5 grid place-items-center"
aria-label={t("aria_labels.auth_forms.clear_email")}
>
<XCircle className="size-5 stroke-custom-text-400" />
</button>
)}
</div>
{emailError?.email && !isFocused && (

View file

@ -45,8 +45,13 @@ export const ForgotPasswordPopover = () => {
>
<span className="flex-shrink-0">🤥</span>
<p className="text-xs">{t("auth.forgot_password.errors.smtp_not_enabled")}</p>
<button type="button" className="flex-shrink-0" onClick={() => close()}>
<X className="h-3 w-3 text-onboarding-text-200" />
<button
type="button"
className="flex-shrink-0 size-3 grid place-items-center"
onClick={() => close()}
aria-label={t("aria_labels.auth_forms.close_popover")}
>
<X className="size-3 text-onboarding-text-200" />
</button>
</div>
)}

View file

@ -167,7 +167,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
<input type="hidden" value={passwordFormData.email} name="email" />
{nextPath && <input type="hidden" value={nextPath} name="next_path" />}
<div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
<label htmlFor="email" className="text-sm font-medium text-onboarding-text-300">
{t("auth.common.email.label")}
</label>
<div
@ -184,21 +184,26 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
disabled
/>
{passwordFormData.email.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
<button
type="button"
className="absolute right-3 size-5"
onClick={handleEmailClear}
/>
aria-label={t("aria_labels.auth_forms.clear_email")}
>
<XCircle className="size-5 stroke-custom-text-400" />
</button>
)}
</div>
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
<label htmlFor="password" className="text-sm text-onboarding-text-300 font-medium">
{mode === EAuthModes.SIGN_IN ? t("auth.common.password.label") : t("auth.common.password.set_password")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword?.password ? "text" : "password"}
id="password"
name="password"
value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
@ -209,29 +214,33 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
autoComplete="on"
autoFocus
/>
{showPassword?.password ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
)}
<button
type="button"
onClick={() => handleShowPassword("password")}
className="absolute right-3 size-5 grid place-items-center"
aria-label={t(
showPassword?.password ? "aria_labels.auth_forms.hide_password" : "aria_labels.auth_forms.show_password"
)}
>
{showPassword?.password ? (
<EyeOff className="size-5 stroke-custom-text-400" />
) : (
<Eye className="size-5 stroke-custom-text-400" />
)}
</button>
</div>
{passwordSupport}
</div>
{mode === EAuthModes.SIGN_UP && (
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
<label htmlFor="confirm-password" className="text-sm text-onboarding-text-300 font-medium">
{t("auth.common.password.confirm_password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword?.retypePassword ? "text" : "password"}
id="confirm-password"
name="confirm_password"
value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
@ -240,17 +249,22 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
/>
{showPassword?.retypePassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
)}
<button
type="button"
className="absolute right-3 size-5 grid place-items-center"
aria-label={t(
showPassword?.retypePassword
? "aria_labels.auth_forms.hide_password"
: "aria_labels.auth_forms.show_password"
)}
onClick={() => handleShowPassword("retypePassword")}
>
{showPassword?.retypePassword ? (
<EyeOff className="size-5 stroke-custom-text-400" />
) : (
<Eye className="size-5 stroke-custom-text-400" />
)}
</button>
</div>
{!!passwordFormData.confirm_password &&
passwordFormData.password !== passwordFormData.confirm_password &&

View file

@ -96,7 +96,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
<input type="hidden" value={uniqueCodeFormData.email} name="email" />
{nextPath && <input type="hidden" value={nextPath} name="next_path" />}
<div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
<label htmlFor="email" className="text-sm font-medium text-onboarding-text-300">
{t("auth.common.email.label")}
</label>
<div
@ -109,25 +109,30 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
value={uniqueCodeFormData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
placeholder={t("auth.common.email.placeholder")}
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`}
className="disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0"
autoComplete="on"
disabled
/>
{uniqueCodeFormData.email.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
<button
type="button"
className="absolute right-3 size-5 grid place-items-center"
aria-label={t("aria_labels.auth_forms.clear_email")}
onClick={handleEmailClear}
/>
>
<XCircle className="size-5 stroke-custom-text-400" />
</button>
)}
</div>
</div>
<div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="code">
<label htmlFor="unique-code" className="text-sm font-medium text-onboarding-text-300">
{t("auth.common.unique_code.label")}
</label>
<Input
name="code"
id="unique-code"
value={uniqueCodeFormData.code}
onChange={(e) => handleFormChange("code", e.target.value)}
placeholder={t("auth.common.unique_code.placeholder")}
@ -142,11 +147,11 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
<button
type="button"
onClick={() => generateNewCode(uniqueCodeFormData.email)}
className={`${
className={
isRequestNewCodeDisabled
? "text-onboarding-text-400"
: "font-medium text-custom-primary-300 hover:text-custom-primary-200"
}`}
}
disabled={isRequestNewCodeDisabled}
>
{resendTimerCode > 0
@ -160,7 +165,13 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
<div className="space-y-2.5">
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isRequestingNewCode ? t("auth.common.unique_code.sending_code") : isSubmitting ? <Spinner height="20px" width="20px" /> : t("common.continue")}
{isRequestingNewCode ? (
t("auth.common.unique_code.sending_code")
) : isSubmitting ? (
<Spinner height="20px" width="20px" />
) : (
t("common.continue")
)}
</Button>
</div>
</form>

View file

@ -8,7 +8,7 @@ type Props = {
export const TermsAndConditions: FC<Props> = (props) => {
const { isSignUp = false } = props;
return (
<span className="flex items-center justify-center py-6">
<div className="flex items-center justify-center py-6">
<p className="text-center text-sm text-onboarding-text-200 whitespace-pre-line">
{isSignUp ? "By creating an account" : "By signing in"}, you agree to our{" \n"}
<Link href="https://plane.so/legals/terms-and-conditions" target="_blank" rel="noopener noreferrer">
@ -20,6 +20,6 @@ export const TermsAndConditions: FC<Props> = (props) => {
</Link>
{"."}
</p>
</span>
</div>
);
};