diff --git a/apps/website/screens/guidelines/localization/LocalizationPage.tsx b/apps/website/screens/guidelines/localization/LocalizationPage.tsx index 7b2ecd52c..b51bc31f2 100644 --- a/apps/website/screens/guidelines/localization/LocalizationPage.tsx +++ b/apps/website/screens/guidelines/localization/LocalizationPage.tsx @@ -82,21 +82,25 @@ const sections = [ - lengthErrorMessage + maxLengthErrorMessage - Min length minLength, max length maxLength. + The maximum length is maxLength. - It is a function that receives two parameters (minlength and maxlength) and returns the text with - those parameters. + It is a function that receives a parameter (maxlength) and returns the text with this parameter. - logoAlternativeText + minLengthErrorMessage + + + The minimum length is minLength. + + + It is a function that receives a parameter (minlength) and returns the text with this parameter. - Logo diff --git a/packages/lib/src/common/utils.ts b/packages/lib/src/common/utils.ts index c861119b5..13ececd8f 100644 --- a/packages/lib/src/common/utils.ts +++ b/packages/lib/src/common/utils.ts @@ -17,3 +17,25 @@ export const getMargin = (marginProp: Space | Margin | undefined, side: Side) => : marginProp && typeof marginProp === "string" ? spaces[marginProp] : "0px"; + +export const getLengthErrorMessage = ({ + value, + minLength, + maxLength, + minLengthErrorMessage, + maxLengthErrorMessage, +}: { + value: string; + minLength?: number; + maxLength?: number; + minLengthErrorMessage: (minLength: number) => string; + maxLengthErrorMessage: (maxLength: number) => string; +}) => { + if (minLength != null && value.length < minLength && minLengthErrorMessage) { + return minLengthErrorMessage(minLength); + } + if (maxLength != null && value.length > maxLength && maxLengthErrorMessage) { + return maxLengthErrorMessage(maxLength); + } + return undefined; +}; diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts index 3195db75c..23e8a1697 100644 --- a/packages/lib/src/common/variables.ts +++ b/packages/lib/src/common/variables.ts @@ -75,8 +75,8 @@ export const defaultTranslatedComponentLabels = { requiredSelectionErrorMessage: "This field is required. Please, choose an option.", requiredValueErrorMessage: "This field is required. Please, enter a value.", formatRequestedErrorMessage: "Please match the format requested.", - lengthErrorMessage: (minLength?: number, maxLength?: number) => `Min length ${minLength}, max length ${maxLength}.`, - logoAlternativeText: "Logo", + minLengthErrorMessage: (minLength: number) => `The minimum length is ${minLength}.`, + maxLengthErrorMessage: (maxLength: number) => `The maximum length is ${maxLength}.`, }, header: { closeIcon: "Close menu", diff --git a/packages/lib/src/text-input/TextInput.test.tsx b/packages/lib/src/text-input/TextInput.test.tsx index c8f234ff4..1aba7d025 100644 --- a/packages/lib/src/text-input/TextInput.test.tsx +++ b/packages/lib/src/text-input/TextInput.test.tsx @@ -2,6 +2,7 @@ import { act, fireEvent, render, waitForElementToBeRemoved } from "@testing-libr import userEvent from "@testing-library/user-event"; import DxcTextInput from "./TextInput"; import MockDOMRect from "../../test/mocks/domRectMock"; +import { HalstackProvider } from "../HalstackContext"; // Mocking DOMRect for Radix Primitive Popover global.DOMRect = MockDOMRect; @@ -157,23 +158,30 @@ describe("TextInput component tests", () => { ); const input = getByRole("textbox"); fireEvent.change(input, { target: { value: "test" } }); - expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "test", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", }); fireEvent.blur(input); - expect(onBlur).toHaveBeenCalled(); expect(onBlur).toHaveBeenCalledWith({ value: "test", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", }); - userEvent.clear(input); + + fireEvent.change(input, { target: { value: "test-maximum-length" } }); + expect(onChange).toHaveBeenCalledWith({ + value: "test-maximum-length", + error: "The maximum length is 10.", + }); + fireEvent.blur(input); + expect(onBlur).toHaveBeenCalledWith({ + value: "test-maximum-length", + error: "The maximum length is 10.", + }); + fireEvent.change(input, { target: { value: "length" } }); - expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "length" }); fireEvent.blur(input); - expect(onBlur).toHaveBeenCalled(); expect(onBlur).toHaveBeenCalledWith({ value: "length" }); }); @@ -195,34 +203,38 @@ describe("TextInput component tests", () => { ); const input = getByRole("textbox"); fireEvent.change(input, { target: { value: "test" } }); - expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "test", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", }); fireEvent.blur(input); - expect(onBlur).toHaveBeenCalled(); expect(onBlur).toHaveBeenCalledWith({ value: "test", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", + }); + fireEvent.change(input, { target: { value: "test-maximum-length" } }); + expect(onChange).toHaveBeenCalledWith({ + value: "test-maximum-length", + error: "The maximum length is 10.", + }); + fireEvent.blur(input); + expect(onBlur).toHaveBeenCalledWith({ + value: "test-maximum-length", + error: "The maximum length is 10.", }); fireEvent.change(input, { target: { value: "tests" } }); - expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "tests", error: "Please match the format requested.", }); fireEvent.blur(input); - expect(onBlur).toHaveBeenCalled(); expect(onBlur).toHaveBeenCalledWith({ value: "tests", error: "Please match the format requested.", }); fireEvent.change(input, { target: { value: "tests4&" } }); - expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "tests4&" }); fireEvent.blur(input); - expect(onBlur).toHaveBeenCalled(); expect(onBlur).toHaveBeenCalledWith({ value: "tests4&" }); }); @@ -732,12 +744,29 @@ describe("TextInput component synchronous autosuggest tests", () => { }); expect(onChange).toHaveBeenCalledWith({ value: "Cha", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", }); fireEvent.blur(input); expect(onBlur).toHaveBeenCalledWith({ value: "Chad", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", + }); + + userEvent.clear(input); + fireEvent.focus(input); + fireEvent.change(input, { target: { value: "Democratic Rep" } }); + expect(getByText("Democratic Rep")).toBeTruthy(); + act(() => { + userEvent.click(getByRole("option")); + }); + expect(onChange).toHaveBeenCalledWith({ + value: "Democratic Rep", + error: "The maximum length is 10.", + }); + fireEvent.blur(input); + expect(onBlur).toHaveBeenCalledWith({ + value: "Democratic Republic of the Congo", + error: "The maximum length is 10.", }); }); @@ -1156,4 +1185,52 @@ describe("TextInput component asynchronous autosuggest tests", () => { await waitForElementToBeRemoved(() => getByText("Searching...")); expect(getByText("Error fetching data")).toBeTruthy(); }); + + test("Maximum and minimum error messages change within HalstackProvider", () => { + const onChange = jest.fn(); + const onBlur = jest.fn(); + const { getByRole } = render( + `Please do not enter more than ${maxLength} characters.`, + minLengthErrorMessage: (minLegth: number) => `Please do not enter less than ${minLegth} characters.`, + }, + }} + > + + + ); + const input = getByRole("textbox"); + fireEvent.change(input, { target: { value: "test" } }); + expect(onChange).toHaveBeenCalledWith({ + value: "test", + error: "Please do not enter less than 5 characters.", + }); + fireEvent.blur(input); + expect(onBlur).toHaveBeenCalledWith({ + value: "test", + error: "Please do not enter less than 5 characters.", + }); + + fireEvent.change(input, { target: { value: "test-maximum-length" } }); + expect(onChange).toHaveBeenCalledWith({ + value: "test-maximum-length", + error: "Please do not enter more than 10 characters.", + }); + fireEvent.blur(input); + expect(onBlur).toHaveBeenCalledWith({ + value: "test-maximum-length", + error: "Please do not enter more than 10 characters.", + }); + }); }); diff --git a/packages/lib/src/text-input/TextInput.tsx b/packages/lib/src/text-input/TextInput.tsx index 2b2911574..71b56b0ba 100644 --- a/packages/lib/src/text-input/TextInput.tsx +++ b/packages/lib/src/text-input/TextInput.tsx @@ -25,7 +25,6 @@ import TextInputPropsType, { AutosuggestWrapperProps, RefType } from "./types"; import { calculateWidth, hasSuggestions, - isLengthIncorrect, isNumberIncorrect, isRequired, makeCancelable, @@ -35,6 +34,7 @@ import HelperText from "../styles/forms/HelperText"; import Label from "../styles/forms/Label"; import ErrorMessage from "../styles/forms/ErrorMessage"; import inputStylesByState from "../styles/forms/inputStylesByState"; +import { getLengthErrorMessage } from "../common/utils"; const TextInputContainer = styled.div<{ margin: TextInputPropsType["margin"]; @@ -232,15 +232,23 @@ const DxcTextInput = forwardRef( setInnerValue(formattedValue); } + const lengthError = getLengthErrorMessage({ + value: formattedValue, + minLength, + maxLength, + minLengthErrorMessage: translatedLabels.formFields.minLengthErrorMessage, + maxLengthErrorMessage: translatedLabels.formFields.maxLengthErrorMessage, + }); + if (isRequired(formattedValue, optional)) { onChange?.({ value: formattedValue, error: translatedLabels.formFields.requiredValueErrorMessage, }); - } else if (isLengthIncorrect(formattedValue, minLength, maxLength)) { + } else if (lengthError) { onChange?.({ value: formattedValue, - error: translatedLabels.formFields.lengthErrorMessage?.(minLength, maxLength), + error: lengthError, }); } else if (patternMismatch(pattern, formattedValue)) { onChange?.({ value: formattedValue, error: translatedLabels.formFields.formatRequestedErrorMessage }); @@ -331,12 +339,20 @@ const DxcTextInput = forwardRef( const handleInputOnBlur = (event: FocusEvent) => { closeSuggestions(); + const lengthError = getLengthErrorMessage({ + value: event.target.value, + minLength, + maxLength, + minLengthErrorMessage: translatedLabels.formFields.minLengthErrorMessage, + maxLengthErrorMessage: translatedLabels.formFields.maxLengthErrorMessage, + }); + if (isRequired(event.target.value, optional)) { onBlur?.({ value: event.target.value, error: translatedLabels.formFields.requiredValueErrorMessage }); - } else if (isLengthIncorrect(event.target.value, minLength, maxLength)) { + } else if (lengthError) { onBlur?.({ value: event.target.value, - error: translatedLabels.formFields.lengthErrorMessage?.(minLength, maxLength), + error: lengthError, }); } else if (patternMismatch(pattern, event.target.value)) { onBlur?.({ value: event.target.value, error: translatedLabels.formFields.formatRequestedErrorMessage }); diff --git a/packages/lib/src/text-input/utils.ts b/packages/lib/src/text-input/utils.ts index 202e0f56c..3f0c73b69 100644 --- a/packages/lib/src/text-input/utils.ts +++ b/packages/lib/src/text-input/utils.ts @@ -48,13 +48,6 @@ export const hasSuggestions = (suggestions: TextInputPropsType["suggestions"]) = export const isRequired = (value: string, optional: boolean) => value === "" && !optional; -export const isLengthIncorrect = ( - value: string, - minLength: TextInputPropsType["minLength"], - maxLength: TextInputPropsType["maxLength"] -) => - value != null && ((minLength != null && value.length < minLength) || (maxLength != null && value.length > maxLength)); - export const isNumberIncorrect = ( value: number, minNumber: TextInputPropsType["minLength"], diff --git a/packages/lib/src/textarea/Textarea.test.tsx b/packages/lib/src/textarea/Textarea.test.tsx index 89a315c53..71c320b3c 100644 --- a/packages/lib/src/textarea/Textarea.test.tsx +++ b/packages/lib/src/textarea/Textarea.test.tsx @@ -196,13 +196,26 @@ describe("Textarea component tests", () => { expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "test", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", }); fireEvent.blur(textarea); expect(onBlur).toHaveBeenCalled(); expect(onBlur).toHaveBeenCalledWith({ value: "test", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", + }); + userEvent.clear(textarea); + fireEvent.change(textarea, { target: { value: "this is a longer value" } }); + expect(onChange).toHaveBeenCalled(); + expect(onChange).toHaveBeenCalledWith({ + value: "this is a longer value", + error: "The maximum length is 10.", + }); + fireEvent.blur(textarea); + expect(onBlur).toHaveBeenCalled(); + expect(onBlur).toHaveBeenCalledWith({ + value: "this is a longer value", + error: "The maximum length is 10.", }); userEvent.clear(textarea); fireEvent.change(textarea, { target: { value: "length" } }); @@ -233,13 +246,13 @@ describe("Textarea component tests", () => { expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "test", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", }); fireEvent.blur(textarea); expect(onBlur).toHaveBeenCalled(); expect(onBlur).toHaveBeenCalledWith({ value: "test", - error: "Min length 5, max length 10.", + error: "The minimum length is 5.", }); fireEvent.change(textarea, { target: { value: "tests" } }); expect(onChange).toHaveBeenCalled(); diff --git a/packages/lib/src/textarea/Textarea.tsx b/packages/lib/src/textarea/Textarea.tsx index b180dcb38..e6fbefff9 100644 --- a/packages/lib/src/textarea/Textarea.tsx +++ b/packages/lib/src/textarea/Textarea.tsx @@ -1,6 +1,6 @@ import { ChangeEvent, FocusEvent, forwardRef, useContext, useEffect, useId, useRef, useState } from "react"; import styled from "@emotion/styled"; -import { getMargin } from "../common/utils"; +import { getLengthErrorMessage, getMargin } from "../common/utils"; import { spaces } from "../common/variables"; import { HalstackLanguageContext } from "../HalstackContext"; import TextareaPropsType, { RefType } from "./types"; @@ -99,21 +99,26 @@ const DxcTextarea = forwardRef( const textareaRef = useRef(null); const prevValueRef = useRef(null); - const isLengthOutOfRange = (value: string) => - value !== "" && minLength && maxLength && (value.length < minLength || value.length > maxLength); - const changeValue = (newValue: string) => { if (value == null) setInnerValue(newValue); + const lengthError = getLengthErrorMessage({ + value: newValue, + minLength, + maxLength, + minLengthErrorMessage: translatedLabels.formFields.minLengthErrorMessage, + maxLengthErrorMessage: translatedLabels.formFields.maxLengthErrorMessage, + }); + if (newValue === "" && !optional) { onChange?.({ value: newValue, error: translatedLabels.formFields.requiredValueErrorMessage, }); - } else if (isLengthOutOfRange(newValue)) { + } else if (lengthError) { onChange?.({ value: newValue, - error: translatedLabels.formFields.lengthErrorMessage?.(minLength, maxLength), + error: lengthError, }); } else if (newValue && pattern && !patternMatch(pattern, newValue)) { onChange?.({ @@ -124,15 +129,22 @@ const DxcTextarea = forwardRef( }; const handleOnBlur = (event: FocusEvent) => { + const lengthError = getLengthErrorMessage({ + value: event.target.value, + minLength, + maxLength, + minLengthErrorMessage: translatedLabels.formFields.minLengthErrorMessage, + maxLengthErrorMessage: translatedLabels.formFields.maxLengthErrorMessage, + }); if (event.target.value === "" && !optional) { onBlur?.({ value: event.target.value, error: translatedLabels.formFields.requiredValueErrorMessage, }); - } else if (isLengthOutOfRange(event.target.value)) { + } else if (lengthError) { onBlur?.({ value: event.target.value, - error: translatedLabels.formFields.lengthErrorMessage?.(minLength, maxLength), + error: lengthError, }); } else if (event.target.value && pattern && !patternMatch(pattern, event.target.value)) { onBlur?.({