Skip to content
2 changes: 2 additions & 0 deletions lib/src/common/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,8 @@ export const componentTokens = {
disabledBorderColorOnDark: CORE_TOKENS.color_grey_500,
disabledContainerFillColor: CORE_TOKENS.color_grey_100,
disabledContainerFillColorOnDark: CORE_TOKENS.color_grey_700,
readOnlyBorderColor: CORE_TOKENS.color_grey_500,
hoverReadOnlyBorderColor: CORE_TOKENS.color_grey_600,
errorBorderColor: CORE_TOKENS.color_red_700,
errorBorderColorOnDark: CORE_TOKENS.color_red_500,
hoverErrorBorderColor: CORE_TOKENS.color_red_600,
Expand Down
8 changes: 8 additions & 0 deletions lib/src/textarea/Textarea.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export const Chromatic = () => (
<Title title="Disabled with value" theme="light" level={4} />
<DxcTextarea label="Disabled" defaultValue="Example text" disabled />
</ExampleContainer>
<ExampleContainer>
<Title title="Read only" theme="light" level={4} />
<DxcTextarea label="Label" readOnly defaultValue="Example text" verticalGrow="manual" />
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-hover">
<Title title="Hovered read only" theme="light" level={4} />
<DxcTextarea label="Label" readOnly defaultValue="Example text" verticalGrow="manual" />
</ExampleContainer>
<ExampleContainer>
<Title title="With error" theme="light" level={4} />
<DxcTextarea
Expand Down
28 changes: 27 additions & 1 deletion lib/src/textarea/Textarea.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("Textarea component tests", () => {
expect(textarea.rows).toBe(10);
});

test("Renders with correct accesibility attributes", () => {
test("Renders with correct accessibility attributes", () => {
const { getByLabelText } = render(<DxcTextarea label="Example label" />);
const textarea = getByLabelText("Example label");
expect(textarea.getAttribute("aria-invalid")).toBe("false");
Expand All @@ -72,6 +72,32 @@ describe("Textarea component tests", () => {
expect(onChange).not.toHaveBeenCalled();
});

test("Read-only textarea does not trigger onChange function", () => {
const onChange = jest.fn();
const { getByLabelText } = render(<DxcTextarea label="Example label" onChange={onChange} readOnly />);
const textarea = getByLabelText("Example label");
userEvent.type(textarea, "Test");
expect(onChange).not.toHaveBeenCalled();
});

test("Read-only textarea sends its value on submit", () => {
const handlerOnSubmit = jest.fn((e) => {
e.preventDefault();
const formData = new FormData(e.target);
const formProps = Object.fromEntries(formData);
expect(formProps).toStrictEqual({ data: "Comments" });
});
const { getByText } = render(
<form onSubmit={handlerOnSubmit}>
<DxcTextarea label="Example label" name="data" defaultValue="Comments" readOnly />
<button type="submit">Submit</button>
</form>
);
const submit = getByText("Submit");
userEvent.click(submit);
expect(handlerOnSubmit).toHaveBeenCalled();
});

test("Not optional constraint (onBlur)", () => {
const onChange = jest.fn();
const onBlur = jest.fn();
Expand Down
76 changes: 36 additions & 40 deletions lib/src/textarea/Textarea.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { useContext, useRef, useState } from "react";
import React, { useContext, useEffect, useRef, useState } from "react";
import styled, { ThemeProvider } from "styled-components";
import { getMargin } from "../common/utils";
import useTheme from "../useTheme";
import useTranslatedLabels from "../useTranslatedLabels";
import { spaces } from "../common/variables";
import { v4 as uuidv4 } from "uuid";
import BackgroundColorContext, { BackgroundColors } from "../BackgroundColorContext";
import { useLayoutEffect } from "react";
import TextareaPropsType, { RefType } from "./types";

const patternMatch = (pattern, value) => new RegExp(pattern).test(value);
Expand All @@ -21,6 +20,7 @@ const DxcTextarea = React.forwardRef<RefType, TextareaPropsType>(
helperText,
placeholder = "",
disabled = false,
readOnly = false,
optional = false,
verticalGrow = "auto",
rows = 4,
Expand Down Expand Up @@ -64,7 +64,15 @@ const DxcTextarea = React.forwardRef<RefType, TextareaPropsType>(
else onChange?.({ value: newValue });
};

const handleTOnBlur = (event) => {
const autoVerticalGrow = () => {
const textareaLineHeight = parseInt(window.getComputedStyle(textareaRef.current)["line-height"]);
const textareaPaddingTopBottom = parseInt(window.getComputedStyle(textareaRef.current)["padding-top"]) * 2;
textareaRef.current.style.height = `${textareaLineHeight * rows}px`;
const newHeight = textareaRef.current.scrollHeight - textareaPaddingTopBottom;
textareaRef.current.style.height = `${newHeight}px`;
};

const handleOnBlur = (event) => {
if (isNotOptional(event.target.value))
onBlur?.({ value: event.target.value, error: translatedLabels.formFields.requiredValueErrorMessage });
else if (isLengthIncorrect(event.target.value))
Expand All @@ -77,20 +85,11 @@ const DxcTextarea = React.forwardRef<RefType, TextareaPropsType>(
else onBlur?.({ value: event.target.value });
};

const handleTOnChange = (event) => {
const handleOnChange = (event) => {
changeValue(event.target.value);
verticalGrow === "auto" && autoVerticalGrow();
};

useLayoutEffect(() => {
if (verticalGrow === "auto") {
const textareaLineHeight = parseInt(window.getComputedStyle(textareaRef.current)["line-height"]);
const textareaPaddingTopBottom = parseInt(window.getComputedStyle(textareaRef.current)["padding-top"]) * 2;
textareaRef.current.style.height = `${textareaLineHeight * rows}px`;
const newHeight = textareaRef.current.scrollHeight - textareaPaddingTopBottom;
textareaRef.current.style.height = `${newHeight}px`;
}
}, [value, verticalGrow, rows, innerValue]);

return (
<ThemeProvider theme={colorsTheme.textarea}>
<TextareaContainer margin={margin} size={size} ref={ref}>
Expand All @@ -111,19 +110,20 @@ const DxcTextarea = React.forwardRef<RefType, TextareaPropsType>(
placeholder={placeholder}
verticalGrow={verticalGrow}
rows={rows}
onChange={handleTOnChange}
onBlur={handleTOnBlur}
onChange={handleOnChange}
onBlur={handleOnBlur}
disabled={disabled}
readOnly={readOnly}
error={error}
minLength={minLength}
maxLength={maxLength}
autoComplete={autocomplete}
backgroundType={backgroundType}
ref={textareaRef}
tabIndex={tabIndex}
aria-invalid={error ? "true" : "false"}
aria-invalid={error ? true : false}
aria-errormessage={error ? errorId : undefined}
aria-required={optional ? "false" : "true"}
aria-required={!disabled && !optional}
/>
{!disabled && typeof error === "string" && (
<Error id={errorId} backgroundType={backgroundType} aria-live={error ? "assertive" : "off"}>
Expand Down Expand Up @@ -215,12 +215,13 @@ const Textarea = styled.textarea<{
backgroundType: BackgroundColors;
error: TextareaPropsType["error"];
}>`
${(props) => {
if (props.verticalGrow === "none") return "resize: none;";
else if (props.verticalGrow === "auto") return `resize: none; overflow: hidden;`;
else if (props.verticalGrow === "manual") return "resize: vertical;";
${({ verticalGrow }) => {
if (verticalGrow === "none") return "resize: none;";
else if (verticalGrow === "auto") return `resize: none; overflow: hidden;`;
else if (verticalGrow === "manual") return "resize: vertical;";
else return `resize: none;`;
}};

${(props) => {
if (props.disabled)
return props.backgroundType === "dark"
Expand All @@ -238,26 +239,28 @@ const Textarea = styled.textarea<{
return props.backgroundType === "dark"
? props.theme.disabledBorderColorOnDark
: props.theme.disabledBorderColor;
else if (props.error) return "transparent";
else if (props.readOnly) return props.theme.readOnlyBorderColor;
else
return props.backgroundType === "dark" ? props.theme.enabledBorderColorOnDark : props.theme.enabledBorderColor;
}};

${(props) =>
props.error &&
!props.disabled &&
`border-color: transparent;
box-shadow: 0 0 0 2px ${
props.backgroundType === "dark" ? props.theme.errorBorderColorOnDark : props.theme.errorBorderColor
};
`box-shadow: 0 0 0 2px ${
props.backgroundType === "dark" ? props.theme.errorBorderColorOnDark : props.theme.errorBorderColor
};
`}

${(props) => props.disabled && "cursor: not-allowed;"};
${(props) =>
!props.disabled &&
`
&:hover {
!props.disabled
? `&:hover {
border-color: ${
props.error
? "transparent"
: props.readOnly
? props.theme.hoverReadOnlyBorderColor
: props.backgroundType === "dark"
? props.theme.hoverBorderColorOnDark
: props.theme.hoverBorderColor
Expand All @@ -271,21 +274,14 @@ const Textarea = styled.textarea<{
};`
}
}
&:focus {
outline: none;
border-color: transparent;
box-shadow: 0 0 0 2px ${
props.backgroundType === "dark" ? props.theme.focusBorderColorOnDark : props.theme.focusBorderColor
};
}
&:focus-within {
&:focus, &:focus-within {
outline: none;
border-color: transparent;
box-shadow: 0 0 0 2px ${
props.backgroundType === "dark" ? props.theme.focusBorderColorOnDark : props.theme.focusBorderColor
};
}
`};
}`
: "cursor: not-allowed;"};

color: ${(props) =>
props.disabled
Expand Down
14 changes: 9 additions & 5 deletions lib/src/textarea/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ type Props = {
* If true, the component will be disabled.
*/
disabled?: boolean;
/**
* If true, the component will not be mutable, meaning the user can not edit the control.
*/
readOnly?: boolean;
/**
* If true, the textarea will be optional, showing '(Optional)'
* next to the label. Otherwise, the field will be considered required
Expand All @@ -59,7 +63,7 @@ type Props = {
* entered is not valid) will be passed to this function.
* If there is no error, error will not be defined.
*/
onChange?: (val: { value: string; error?: string}) => void;
onChange?: (val: { value: string; error?: string }) => void;
/**
* This function will be called when the textarea loses the focus. An
* object including the textarea value and the error (if the value entered
Expand All @@ -68,11 +72,11 @@ type Props = {
*/
onBlur?: (val: { value: string; error?: string }) => void;
/**
* If it is a defined value and also a truthy string, the component will
* change its appearance, showing the error below the textarea. If the
* If it is a defined value and also a truthy string, the component will
* change its appearance, showing the error below the textarea. If the
* defined value is an empty string, it will reserve a space below the
* component for a future error, but it would not change its look. In
* case of being undefined or null, both the appearance and the space for
* component for a future error, but it would not change its look. In
* case of being undefined or null, both the appearance and the space for
* the error message would not be modified.
*/
error?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import Example from "@/common/example/Example";
import controlled from "./examples/controlled";
import uncontrolled from "./examples/uncontrolled";
import HeaderDescriptionCell from "@/common/HeaderDescriptionCell";
import StatusTag from "@/common/StatusTag";

const sections = [
{
Expand Down
19 changes: 7 additions & 12 deletions website/screens/components/file-input/code/FileInputCodePage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
DxcFlex,
DxcTable,
DxcLink,
DxcGrid,
} from "@dxc-technology/halstack-react";
import { DxcFlex, DxcTable, DxcLink } from "@dxc-technology/halstack-react";
import QuickNavContainerLayout from "@/common/QuickNavContainerLayout";
import QuickNavContainer from "@/common/QuickNavContainer";
import Code from "@/common/Code";
Expand All @@ -29,14 +24,14 @@ const sections = [
</thead>
<tbody>
<tr>
<td>name: string</td>
<td></td>
<td>
<DxcGrid gap="0.25rem" placeItems="start">
<StatusTag status="Deprecated">Deprecated</StatusTag>
Name attribute.
</DxcGrid>
<DxcFlex direction="column" gap="0.25rem" alignItems="baseline">
<StatusTag status="Deprecated">Deprecated</StatusTag>name:
string
</DxcFlex>
</td>
<td></td>
<td>Name attribute.</td>
</tr>
<tr>
<td>mode: 'file' | 'filedrop' | 'dropzone'</td>
Expand Down
1 change: 0 additions & 1 deletion website/screens/components/slider/code/SliderCodePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import controlled from "./examples/controlled";
import uncontrolled from "./examples/uncontrolled";
import formatLabel from "./examples/formatLabel";
import HeaderDescriptionCell from "@/common/HeaderDescriptionCell";
import StatusTag from "@/common/StatusTag";

const sections = [
{
Expand Down
15 changes: 15 additions & 0 deletions website/screens/components/textarea/code/TextareaCodePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import controlled from "./examples/controlled";
import uncontrolled from "./examples/uncontrolled";
import errorHandling from "./examples/errorHandling";
import HeaderDescriptionCell from "@/common/HeaderDescriptionCell";
import StatusTag from "@/common/StatusTag";

const sections = [
{
Expand Down Expand Up @@ -77,6 +78,20 @@ const sections = [
been filled.
</td>
</tr>
<tr>
<td>
<DxcFlex direction="column" gap="0.25rem" alignItems="baseline">
<StatusTag status="Information">New</StatusTag>readOnly:
boolean
</DxcFlex>
</td>
<td>
<Code>false</Code>
</td>
<td>
If true, the component will not be mutable, meaning the user can not edit the control.
</td>
</tr>
<tr>
<td>verticalGrow: 'auto' | 'manual' | 'none'</td>
<td>
Expand Down