Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Form fields): improve accesibility of errors #1246

Merged
merged 13 commits into from
Sep 26, 2024
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in desktop, the text is not aligned in the same way as mobile, @aweell is this expected?

is it possible to align the text line to the middle of the icon? (compare with mobile screenshot to see the difference)
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not expected, but also not correctly documented in the specs

the size of the icon could be 1lh to match the line height in mobile (16) but also in desktop (20) so even if the font-size increases appears aligned to the first line of text? this approach seems correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed size to 1lh (fallback to 1rem for old browsers not supporting lh unit)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/__stories__/custom-field-story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ const AutocompleteSelectField = ({name, options}: AutocompleteSelectFieldProps)

const fieldProps = useFieldProps({
name,
label: 'Autocomplete',
defaultValue: undefined,
value: filterValue,
processValue: (value: string) => value.trim(),
Expand Down Expand Up @@ -249,7 +250,6 @@ const AutocompleteSelectField = ({name, options}: AutocompleteSelectFieldProps)
<TextField
{...fieldProps}
fullWidth
label="Autocomplete"
ref={combineRefs(ref, inputRef)}
onFocus={onPress}
onPress={() => {
Expand Down
6 changes: 4 additions & 2 deletions src/box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Props = {
paddingBottom?: ByBreakpoint<PadSize>;
paddingLeft?: ByBreakpoint<PadSize>;
paddingRight?: ByBreakpoint<PadSize>;
as?: React.ComponentType<any> | string;
children?: React.ReactNode;
className?: string;
role?: string;
Expand All @@ -31,6 +32,7 @@ const Box = React.forwardRef<HTMLDivElement, Props>(
{
className,
children,
as: Component = 'div',
width,
padding = 0,
paddingX = padding,
Expand Down Expand Up @@ -64,7 +66,7 @@ const Box = React.forwardRef<HTMLDivElement, Props>(
}

return (
<div
<Component
{...getPrefixedDataAttributes(dataAttributes)}
role={role}
aria-label={ariaLabel}
Expand All @@ -78,7 +80,7 @@ const Box = React.forwardRef<HTMLDivElement, Props>(
id={id}
>
{children}
</div>
</Component>
);
}
);
Expand Down
5 changes: 2 additions & 3 deletions src/credit-card-expiration-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const CreditCardExpirationField = ({
error,
helperText,
name,
label,
optional,
validate: validateProp,
onChange,
Expand All @@ -112,9 +113,6 @@ const CreditCardExpirationField = ({
const {setFormError, jumpToNext} = useForm();

const validate = (value: ExpirationDateValue, rawValue: string): string | undefined => {
if (!rawValue) {
return optional ? '' : texts.formFieldErrorIsMandatory || t(tokens.formFieldErrorIsMandatory);
}
Comment on lines -115 to -117
Copy link
Contributor Author

@atabel atabel Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found this code repeated in most input fields, I've moved it inside useFieldProps

const {month, year} = value;
if (!month || !year) {
return texts.formCreditCardExpirationError || t(tokens.formCreditCardExpirationError);
Expand Down Expand Up @@ -142,6 +140,7 @@ const CreditCardExpirationField = ({

const fieldProps = useFieldProps({
name,
label,
value,
defaultValue,
processValue,
Expand Down
7 changes: 3 additions & 4 deletions src/credit-card-number-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const CreditCardNumberField = ({
error,
helperText,
name,
label,
optional,
validate: validateProp,
onChange,
Expand All @@ -174,9 +175,6 @@ const CreditCardNumberField = ({

const validate = (value: string | undefined, rawValue: string) => {
const error = texts.formCreditCardNumberError || t(tokens.formCreditCardNumberError);
if (!value) {
return optional ? '' : texts.formFieldErrorIsMandatory || t(tokens.formFieldErrorIsMandatory);
}
if (isAmericanExpress(value) && !acceptedCards.americanExpress) {
return error;
}
Expand All @@ -189,7 +187,7 @@ const CreditCardNumberField = ({
if (!isValidCreditCardNumber(value)) {
return error;
}
if (getCreditCardNumberLength(value) !== value.length) {
if (getCreditCardNumberLength(value) !== value?.length) {
return error;
}
return validateProp?.(value, rawValue);
Expand All @@ -199,6 +197,7 @@ const CreditCardNumberField = ({

const fieldProps = useFieldProps({
name,
label,
value,
defaultValue,
processValue,
Expand Down
6 changes: 2 additions & 4 deletions src/cvv-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const TooltipContent = ({acceptedCards}: {acceptedCards: CardOptions}) => {
<Text2>
{texts.formCreditCardCvvTooltipAmex || t(tokens.formCreditCardCvvTooltipAmex)}
</Text2>
)
Copy link
Contributor Author

@atabel atabel Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

</Inline>
)}
</Stack>
Expand All @@ -61,6 +60,7 @@ const CvvField = ({
error,
helperText,
name,
label,
optional,
validate: validateProp,
onChange,
Expand All @@ -79,9 +79,6 @@ const CvvField = ({
const [isCvvHelpOpen, setIsCvvHelpOpen] = React.useState(false);

const validate = (value: string, rawValue: string) => {
if (!value) {
return optional ? '' : texts.formFieldErrorIsMandatory || t(tokens.formFieldErrorIsMandatory);
}
if (value.length !== maxLength) {
return texts.formCreditCardCvvError || t(tokens.formCreditCardCvvError);
}
Expand All @@ -92,6 +89,7 @@ const CvvField = ({

const fieldProps = useFieldProps({
name,
label,
value,
defaultValue,
processValue,
Expand Down
2 changes: 2 additions & 0 deletions src/date-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const DateField = ({
error,
helperText,
name,
label,
optional,
validate: validateProp,
onChange,
Expand Down Expand Up @@ -77,6 +78,7 @@ const DateField = ({

const fieldProps = useFieldProps({
name,
label,
value,
defaultValue,
processValue,
Expand Down
2 changes: 2 additions & 0 deletions src/date-time-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const FormDateField = ({
error,
helperText,
name,
label,
optional,
validate: validateProp,
onChange,
Expand Down Expand Up @@ -83,6 +84,7 @@ const FormDateField = ({

const fieldProps = useFieldProps({
name,
label,
value,
defaultValue,
processValue,
Expand Down
14 changes: 3 additions & 11 deletions src/decimal-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {TextFieldBaseAutosuggest} from './text-field-base';
import {createChangeEvent} from './utils/dom';
import {useRifm} from 'rifm';
import {combineRefs} from './utils/common';
import * as tokens from './text-tokens';

import type {Locale} from './utils/locale';
import type {CommonFormFieldProps} from './text-field-base';
Expand Down Expand Up @@ -115,8 +114,9 @@ const DecimalField = ({
error,
helperText,
name,
label,
optional,
validate: validateProp,
validate,
onChange,
onChangeValue,
onBlur,
Expand All @@ -126,19 +126,11 @@ const DecimalField = ({
dataAttributes,
...rest
}: DecimalFieldProps): JSX.Element => {
const {texts, t} = useTheme();

const validate = (value: string | undefined, rawValue: string) => {
if (!value) {
return optional ? '' : texts.formFieldErrorIsMandatory || t(tokens.formFieldErrorIsMandatory);
}
return validateProp?.(value, rawValue);
};

const processValue = (value: string) => value.trim();

const fieldProps = useFieldProps({
name,
label,
value,
defaultValue,
processValue,
Expand Down
7 changes: 3 additions & 4 deletions src/email-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const EmailField = ({
error,
helperText,
name,
label,
optional,
validate: validateProp,
onChange,
Expand All @@ -34,10 +35,7 @@ const EmailField = ({
const {texts, t} = useTheme();

const validate = (value: string | undefined, rawValue: string) => {
if (!value) {
return optional ? '' : texts.formFieldErrorIsMandatory || t(tokens.formFieldErrorIsMandatory);
}
if (!RE_EMAIL.test(value)) {
if (!RE_EMAIL.test(value ?? '')) {
return texts.formEmailError || t(tokens.formEmailError);
}
return validateProp?.(value, rawValue);
Expand All @@ -47,6 +45,7 @@ const EmailField = ({

const fieldProps = useFieldProps({
name,
label,
value,
defaultValue,
processValue,
Expand Down
23 changes: 21 additions & 2 deletions src/form-context.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';
import * as React from 'react';
import {useTheme} from './hooks';
import * as tokens from './text-tokens';

export type FormStatus = 'filling' | 'sending';
export type FormErrors = {[name: string]: string | undefined};
Expand All @@ -9,6 +11,7 @@ export type FieldRegistration = {
input?: HTMLInputElement | HTMLSelectElement | null;
validator?: FieldValidator;
focusableElement?: HTMLDivElement | HTMLSelectElement | null;
label?: string;
};

type Context = {
Expand Down Expand Up @@ -47,18 +50,21 @@ export const useForm = (): Context => React.useContext(FormContext);

export const useControlProps = <T,>({
name,
label,
value,
defaultValue,
onChange,
disabled,
}: {
name: string;
label?: string;
value: undefined | T;
defaultValue: undefined | T;
onChange: undefined | ((value: T) => void);
disabled?: boolean;
}): {
name: string;
label?: string;
value?: T;
defaultValue?: T;
onChange: (value: T) => void;
Expand All @@ -77,11 +83,13 @@ export const useControlProps = <T,>({

return {
name,
label,
value,
defaultValue: defaultValue ?? (value === undefined ? rawValues[name] ?? false : undefined),
focusableRef: (focusableElement: HTMLDivElement | null) =>
register(name, {
focusableElement,
label,
}),
onChange: (value: T) => {
setRawValue({name, value});
Expand All @@ -95,6 +103,7 @@ export const useControlProps = <T,>({

export const useFieldProps = ({
name,
label,
value,
defaultValue,
processValue,
Expand All @@ -108,6 +117,7 @@ export const useFieldProps = ({
onChangeValue,
}: {
name: string;
label: string;
value?: string;
defaultValue?: string;
processValue: (value: string) => unknown;
Expand All @@ -123,6 +133,7 @@ export const useFieldProps = ({
value?: string;
defaultValue?: string;
name: string;
label: string;
helperText?: string;
required: boolean;
error: boolean;
Expand All @@ -131,6 +142,7 @@ export const useFieldProps = ({
inputRef: (field: HTMLInputElement | null) => void;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
} => {
const {texts, t} = useTheme();
const {setRawValue, setValue, rawValues, values, formErrors, formStatus, setFormError, register} =
useForm();
const rawValue = value ?? defaultValue ?? rawValues[name] ?? '';
Expand All @@ -151,15 +163,22 @@ export const useFieldProps = ({
value,
defaultValue: defaultValue ?? (value === undefined ? rawValues[name] ?? '' : undefined),
name,
label,
helperText: formErrors[name] || helperText,
required: !optional,
error: error || !!formErrors[name],
disabled: disabled || formStatus === 'sending',
onBlur: (e: React.FocusEvent) => {
setFormError({name, error: validate?.(values[name], rawValues[name])});
let error: string | undefined;
if (!rawValues[name] && !optional) {
error = texts.formFieldErrorIsMandatory || t(tokens.formFieldErrorIsMandatory);
} else if (validate) {
error = validate(values[name], rawValues[name]);
}
setFormError({name, error});
onBlur?.(e);
},
inputRef: (input: HTMLInputElement | null) => register(name, {input, validator: validate}),
inputRef: (input: HTMLInputElement | null) => register(name, {input, validator: validate, label}),
Copy link
Contributor Author

@atabel atabel Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need the field label to be in the registrations map, to be able to use it when rendering the global form error: "Revisa los errores en los siguientes campos: nombre, email". See changes in form.tsx

onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
const rawValue = event.currentTarget.value;
const value = processValue(rawValue);
Expand Down
Loading
Loading