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

Add readOnly prop to the Text Input and Date Input #1688

Merged
merged 3 commits into from Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/src/common/variables.ts
Expand Up @@ -1051,6 +1051,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
10 changes: 10 additions & 0 deletions lib/src/date-input/DateInput.stories.tsx
Expand Up @@ -41,6 +41,16 @@ const DateInputChromatic = () => (
disabled
/>
</ExampleContainer>
<ExampleContainer>
<Title title="Read only" theme="light" level={4} />
<DxcDateInput
label="Example label"
helperText="Help message"
defaultValue="06-04-2007"
clearable
readOnly
/>
</ExampleContainer>
<ExampleContainer>
<Title title="Invalid" theme="light" level={4} />
<DxcDateInput label="Error date input" error="Error message." placeholder />
Expand Down
7 changes: 7 additions & 0 deletions lib/src/date-input/DateInput.test.js
Expand Up @@ -43,6 +43,13 @@ describe("DateInput component tests", () => {
expect(getByText("Personalized error.")).toBeTruthy();
});

test("Read-only variant doesn't open the calendar", () => {
const { getByRole, queryByRole } = render(<DxcDateInput value="20-10-2019" readOnly />);
const calendarAction = getByRole("combobox");
userEvent.click(calendarAction);
expect(queryByRole("dialog")).toBeFalsy();
});

test("Renders with an initial value when it is uncontrolled", () => {
const { getByText, getByRole } = render(
<DxcDateInput label="Default label" placeholder="Placeholder" defaultValue="21-10-2015" />
Expand Down
12 changes: 7 additions & 5 deletions lib/src/date-input/DateInput.tsx
Expand Up @@ -45,6 +45,7 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
placeholder = false,
clearable,
disabled,
readOnly,
optional,
onChange,
onBlur,
Expand Down Expand Up @@ -75,7 +76,7 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
if (value || value === "") setDayjsDate(getDate(value, format, lastValidYear, setLastValidYear));
}, [value, format, lastValidYear]);

useLayoutEffect(() => {
useEffect(() => {
if (!disabled) {
const actionButtonRef = dateRef?.current.querySelector("[title='Open calendar']");
actionButtonRef?.setAttribute("aria-haspopup", true);
Expand Down Expand Up @@ -103,7 +104,7 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
});
};

const handleIOnChange = ({ value: newValue, error: inputError }) => {
const handleOnChange = ({ value: newValue, error: inputError }) => {
value ?? setInnerValue(newValue);
const newDate = getDate(newValue, format, lastValidYear, setLastValidYear);
const invalidDateMessage =
Expand All @@ -124,7 +125,7 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
setDayjsDate(null);
}
};
const handleIOnBlur = ({ value, error: inputError }) => {
const handleOnBlur = ({ value, error: inputError }) => {
const date = getDate(value, format, lastValidYear, setLastValidYear);
const invalidDateMessage = value !== "" && !date.isValid() && translatedLabels.dateInput.invalidDateErrorMessage;
const callbackParams =
Expand Down Expand Up @@ -172,9 +173,10 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
}}
clearable={clearable}
disabled={disabled}
readOnly={readOnly}
optional={optional}
onChange={handleIOnChange}
onBlur={handleIOnBlur}
onChange={handleOnChange}
onBlur={handleOnBlur}
error={error}
autocomplete={autocomplete}
margin={margin}
Expand Down
4 changes: 4 additions & 0 deletions lib/src/date-input/types.ts
Expand Up @@ -45,6 +45,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 date will be optional, showing '(Optional)'
* next to the label. Otherwise, the field will be considered required and an error will be
Expand Down
39 changes: 39 additions & 0 deletions lib/src/text-input/TextInput.stories.tsx
Expand Up @@ -186,6 +186,45 @@ export const Chromatic = () => (
action={action}
/>
</ExampleContainer>
<ExampleContainer>
<Title title="Read only" theme="light" level={4} />
<DxcTextInput
label="Example label"
helperText="Help message"
clearable
readOnly
optional
prefix="+34"
defaultValue="Text"
action={action}
/>
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-hover">
<Title title="Hovered read only" theme="light" level={4} />
<DxcTextInput
label="Example label"
helperText="Help message"
clearable
readOnly
optional
prefix="+34"
defaultValue="Text"
action={action}
/>
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-active">
<Title title="Active read only" theme="light" level={4} />
<DxcTextInput
label="Example label"
helperText="Help message"
clearable
readOnly
optional
prefix="+34"
defaultValue="Text"
action={action}
/>
</ExampleContainer>
<BackgroundColorProvider color="#333333">
<DarkContainer>
<Title title="Dark theme" theme="dark" level={2} />
Expand Down
74 changes: 68 additions & 6 deletions lib/src/text-input/TextInput.test.js
Expand Up @@ -206,6 +206,7 @@ describe("TextInput component tests", () => {
expect(onChange).toHaveBeenCalled();
expect(onChange).toHaveBeenCalledWith({ value: "onchange event test" });
});

test("onBlur function is called correctly", () => {
const onBlur = jest.fn();
const onChange = jest.fn();
Expand All @@ -231,15 +232,15 @@ describe("TextInput component tests", () => {
expect(input.value).toBe("");
});

test("Disabled input renders with correct aria and can not be modified", () => {
test("Disabled text input renders with correct aria and can not be modified", () => {
const onChange = jest.fn();
const { getByRole } = render(<DxcTextInput label="Input label" onChange={onChange} disabled />);
const input = getByRole("textbox");
userEvent.type(input, "Test");
expect(onChange).not.toHaveBeenCalled();
});

test("Disabled input (action must be shown but not clickable)", () => {
test("Disabled text input (action must be shown but not clickable)", () => {
const onClick = jest.fn();
const action = {
onClick: onClick,
Expand All @@ -263,7 +264,8 @@ describe("TextInput component tests", () => {
userEvent.click(getByRole("button"));
expect(onClick).not.toHaveBeenCalled();
});
test("Disabled input (clear default action should not be displayed, even with text written on the input)", () => {

test("Disabled text input (clear default action should not be displayed, even with text written on the input)", () => {
const { getByRole, queryByRole } = render(
<DxcTextInput label="Disabled input label" value="Sample text" disabled clearable />
);
Expand All @@ -272,7 +274,7 @@ describe("TextInput component tests", () => {
expect(queryByRole("button")).toBeFalsy();
});

test("Disabled input (suffix and preffix must be displayed)", () => {
test("Disabled text input (suffix and prefix must be displayed)", () => {
const { getByRole, getByText } = render(
<DxcTextInput label="Disabled input label" value="Sample text" prefix="+34" suffix="USD" disabled />
);
Expand All @@ -282,6 +284,65 @@ describe("TextInput component tests", () => {
expect(getByText("USD")).toBeTruthy();
});

test("Read-only text input doesn't render the clear action", () => {
const { getByRole, queryByRole } = render(
<DxcTextInput label="Disabled input label" defaultValue="Sample text" readOnly clearable />
);
const input = getByRole("textbox");
expect(input.readOnly).toBeTruthy();
expect(queryByRole("button")).toBeFalsy();
});

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

test("Read-only text input 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: "Text" });
});
const { getByText } = render(
<form onSubmit={handlerOnSubmit}>
<DxcTextInput label="Example label" name="data" defaultValue="Text" readOnly />
<button type="submit">Submit</button>
</form>
);
const submit = getByText("Submit");
userEvent.click(submit);
expect(handlerOnSubmit).toHaveBeenCalled();
});

test("Read-only text input doesn't trigger custom action's onClick event", () => {
const onClick = jest.fn();
const action = {
onClick: onClick,
icon: (
<svg
data-testid="image"
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="currentColor"
>
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
</svg>
),
title: "Search",
};
const { getByRole } = render(<DxcTextInput label="Input label" action={action} readOnly />);
userEvent.click(getByRole("button"));
expect(onClick).not.toHaveBeenCalled();
});

test("Action prop: image displayed with title and onClick event", () => {
const onClick = jest.fn();
const action = {
Expand Down Expand Up @@ -356,7 +417,7 @@ describe("TextInput component tests", () => {
expect(getByText("USD")).toBeTruthy();
});

test("Text Input has correct aria accesibility attributes", () => {
test("Text Input has correct aria accessibility attributes", () => {
const onClick = jest.fn();
const action = {
onClick: onClick,
Expand Down Expand Up @@ -391,7 +452,8 @@ describe("TextInput component tests", () => {
const search = getAllByRole("button")[1];
expect(search.getAttribute("aria-label")).toBe("Search");
});
test("Autosuggest has correct accesibility attributes", () => {

test("Autosuggest has correct accessibility attributes", () => {
const { getByRole, getAllByRole } = render(
<DxcTextInput label="Autocomplete Countries" optional suggestions={countries} />
);
Expand Down
21 changes: 14 additions & 7 deletions lib/src/text-input/TextInput.tsx
Expand Up @@ -69,6 +69,7 @@ const DxcTextInput = React.forwardRef<RefType, TextInputPropsType>(
action,
clearable = false,
disabled = false,
readOnly = false,
optional = false,
prefix = "",
suffix = "",
Expand Down Expand Up @@ -348,6 +349,7 @@ const DxcTextInput = React.forwardRef<RefType, TextInputPropsType>(
<InputContainer
error={error ? true : false}
disabled={disabled}
readOnly={readOnly}
backgroundType={backgroundType}
onClick={handleInputContainerOnClick}
onMouseDown={handleInputContainerOnMouseDown}
Expand Down Expand Up @@ -406,6 +408,7 @@ const DxcTextInput = React.forwardRef<RefType, TextInputPropsType>(
event.stopPropagation();
}}
disabled={disabled}
readOnly={readOnly}
ref={inputRef}
backgroundType={backgroundType}
pattern={pattern}
Expand All @@ -426,15 +429,15 @@ const DxcTextInput = React.forwardRef<RefType, TextInputPropsType>(
}
aria-invalid={error ? true : false}
aria-errormessage={error ? errorId : undefined}
aria-required={optional ? false : true}
aria-required={!disabled && !optional}
/>
</AutosuggestWrapper>
{!disabled && error && (
<ErrorIcon backgroundType={backgroundType} aria-label="Error">
{icons.error}
</ErrorIcon>
)}
{!disabled && clearable && (value ?? innerValue).length > 0 && (
{!disabled && !readOnly && clearable && (value ?? innerValue).length > 0 && (
<Action
aria-label={translatedLabels.textInput.clearFieldActionTitle}
onClick={handleClearActionOnClick}
Expand Down Expand Up @@ -487,7 +490,7 @@ const DxcTextInput = React.forwardRef<RefType, TextInputPropsType>(
<Action
aria-label={action.title}
disabled={disabled}
onClick={action.onClick}
onClick={!readOnly ? action.onClick : undefined}
onMouseDown={(event) => {
event.stopPropagation();
}}
Expand Down Expand Up @@ -579,6 +582,7 @@ const HelperText = styled.span<{ disabled: TextInputPropsType["disabled"]; backg

const InputContainer = styled.div<{
disabled: TextInputPropsType["disabled"];
readOnly: TextInputPropsType["readOnly"];
error: boolean;
backgroundType: BackgroundColors;
}>`
Expand All @@ -602,6 +606,7 @@ const InputContainer = styled.div<{
return props.backgroundType === "dark"
? props.theme.disabledBorderColorOnDark
: props.theme.disabledBorderColor;
else if (props.readOnly) return props.theme.readOnlyBorderColor;
else
return props.backgroundType === "dark" ? props.theme.enabledBorderColorOnDark : props.theme.enabledBorderColor;
}};
Expand All @@ -613,15 +618,16 @@ const InputContainer = styled.div<{
props.backgroundType === "dark" ? props.theme.errorBorderColorOnDark : props.theme.errorBorderColor
};
`}
${(props) => props.disabled && "cursor: not-allowed;"};

${(props) =>
!props.disabled &&
`
!props.disabled
? `
&:hover {
border-color: ${
props.error
? "transparent"
: props.readOnly
? props.theme.hoverReadOnlyBorderColor
: props.backgroundType === "dark"
? props.theme.hoverBorderColorOnDark
: props.theme.hoverBorderColor
Expand All @@ -642,7 +648,8 @@ const InputContainer = styled.div<{
props.backgroundType === "dark" ? props.theme.focusBorderColorOnDark : props.theme.focusBorderColor
};
}
`};
`
: "cursor: not-allowed;"};
`;

const Input = styled.input<{ backgroundType: BackgroundColors }>`
Expand Down
4 changes: 4 additions & 0 deletions lib/src/text-input/types.ts
Expand Up @@ -60,6 +60,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.
GomezIvann marked this conversation as resolved.
Show resolved Hide resolved
*/
readOnly?: boolean;
/**
* If true, the input will be optional, showing '(Optional)'
* next to the label. Otherwise, the field will be considered required and an error will be
Expand Down