Skip to content

Commit

Permalink
Merge pull request #1693 from dxc-technology/gomezivann/readonly-chec…
Browse files Browse the repository at this point in the history
…kbox

Add `readOnly` prop to Checkbox
  • Loading branch information
raquelarrojo committed Oct 2, 2023
2 parents 2bca710 + ff7a4d4 commit 752ca0a
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 315 deletions.
16 changes: 16 additions & 0 deletions lib/src/checkbox/Checkbox.stories.tsx
Expand Up @@ -38,6 +38,22 @@ const Checkbox = () => (
<Title title="Disabled, checked and optional" theme="light" level={4} />
<DxcCheckbox label="Checkbox" disabled defaultChecked optional />
</ExampleContainer>
<ExampleContainer>
<Title title="Read-only" theme="light" level={4} />
<DxcCheckbox label="Checkbox" readOnly />
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-hover">
<Title title="Hovered read-only" theme="light" level={4} />
<DxcCheckbox label="Checkbox" readOnly />
</ExampleContainer>
<ExampleContainer>
<Title title="Read-only, checked and optional" theme="light" level={4} />
<DxcCheckbox label="Checkbox" readOnly defaultChecked optional />
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-hover">
<Title title="Hovered read-only and checked" theme="light" level={4} />
<DxcCheckbox label="Checkbox" readOnly defaultChecked optional />
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-focus">
<Title title="Focused" theme="light" level={4} />
<DxcCheckbox label="Focused" />
Expand Down
41 changes: 40 additions & 1 deletion lib/src/checkbox/Checkbox.test.js
Expand Up @@ -9,20 +9,59 @@ describe("Checkbox component tests", () => {
const labelId = getByText("Checkbox").getAttribute("id");
expect(getByRole("checkbox").getAttribute("aria-labelledby")).toBe(labelId);
expect(getByRole("checkbox").getAttribute("aria-required")).toBe("true");
expect(getByRole("checkbox").getAttribute("aria-readonly")).toBe("false");
expect(getByRole("checkbox").getAttribute("aria-disabled")).toBe("false");
});

test("Optional checkbox renders with correct aria-required", () => {
const { getByRole } = render(<DxcCheckbox label="Checkbox" optional />);
expect(getByRole("checkbox").getAttribute("aria-required")).toBe("false");
});

test("Calls correct function on click", () => {
test("Calls correct function onChange", () => {
const onChange = jest.fn();
const { getByText } = render(<DxcCheckbox label="Checkbox" onChange={onChange} />);
fireEvent.click(getByText("Checkbox"));
expect(onChange).toHaveBeenCalled();
});

test("Read-only checkbox does not trigger onChange function", () => {
const onChange = jest.fn();
const { getByRole } = render(<DxcCheckbox label="Checkbox" onChange={onChange} readOnly />);
const checkbox = getByRole("checkbox");
expect(checkbox.getAttribute("aria-readonly")).toBe("true");
fireEvent.click(checkbox);
expect(onChange).not.toHaveBeenCalled();
});

test("Read-only checkbox 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: "checked" });
});
const { getByText } = render(
<form onSubmit={handlerOnSubmit}>
<DxcCheckbox label="Checkbox" name="data" value="checked" readOnly defaultChecked />
<button type="submit">Submit</button>
</form>
);
const submit = getByText("Submit");
userEvent.click(submit);
expect(handlerOnSubmit).toHaveBeenCalled();
});

test("Read-only checkbox doesn't change its value with Space key", () => {
const onChange = jest.fn();
const { getByRole } = render(<DxcCheckbox label="Checkbox" onChange={onChange} readOnly />);
const checkbox = getByRole("checkbox");
userEvent.tab();
expect(document.activeElement === checkbox).toBeTruthy();
fireEvent.keyDown(checkbox, { key: " ", code: "Space", keyCode: 32, charCode: 32 });
expect(onChange).not.toHaveBeenCalled();
});

test("Uncontrolled checkbox", () => {
const onChange = jest.fn();
const component = render(<DxcCheckbox label="Checkbox" onChange={onChange} name="test" />);
Expand Down
138 changes: 80 additions & 58 deletions lib/src/checkbox/Checkbox.tsx
Expand Up @@ -25,6 +25,7 @@ const DxcCheckbox = React.forwardRef<RefType, CheckboxPropsType>(
name = "",
disabled = false,
optional = false,
readOnly = false,
onChange,
margin,
size = "fitContent",
Expand All @@ -42,10 +43,12 @@ const DxcCheckbox = React.forwardRef<RefType, CheckboxPropsType>(
const translatedLabels = useTranslatedLabels();

const handleCheckboxChange = () => {
document.activeElement !== checkboxRef?.current && checkboxRef?.current?.focus();
const newChecked = checked ?? innerChecked;
checked ?? setInnerChecked((innerChecked) => !innerChecked);
onChange?.(!newChecked);
if (!disabled && !readOnly) {
document.activeElement !== checkboxRef?.current && checkboxRef?.current?.focus();
const newChecked = checked ?? innerChecked;
checked ?? setInnerChecked((innerChecked) => !innerChecked);
onChange?.(!newChecked);
}
};

const handleKeyboard = (event) => {
Expand All @@ -60,15 +63,21 @@ const DxcCheckbox = React.forwardRef<RefType, CheckboxPropsType>(
<ThemeProvider theme={colorsTheme.checkbox}>
<MainContainer
disabled={disabled}
onClick={disabled ? undefined : handleCheckboxChange}
readOnly={readOnly}
onClick={handleCheckboxChange}
margin={margin}
size={size}
checked={checked ?? innerChecked}
backgroundType={backgroundType}
ref={ref}
>
{label && labelPosition === "before" && (
<LabelContainer id={labelId} disabled={disabled} backgroundType={backgroundType}>
{label && (
<LabelContainer
id={labelId}
disabled={disabled}
backgroundType={backgroundType}
labelPosition={labelPosition}
>
{label}
{optional && ` ${translatedLabels.formFields.optionalLabel}`}
</LabelContainer>
Expand All @@ -89,22 +98,18 @@ const DxcCheckbox = React.forwardRef<RefType, CheckboxPropsType>(
tabIndex={disabled ? -1 : tabIndex}
aria-checked={checked ?? innerChecked}
aria-disabled={disabled}
aria-readonly={readOnly}
aria-required={!disabled && !optional}
aria-labelledby={labelId}
backgroundType={backgroundType}
checked={checked ?? innerChecked}
disabled={disabled}
readOnly={readOnly}
ref={checkboxRef}
>
{(checked ?? innerChecked) && checkedIcon}
</Checkbox>
</CheckboxContainer>
{label && labelPosition === "after" && (
<LabelContainer id={labelId} disabled={disabled} backgroundType={backgroundType}>
{optional && `${translatedLabels.formFields.optionalLabel} `}
{label}
</LabelContainer>
)}
</MainContainer>
</ThemeProvider>
);
Expand All @@ -119,64 +124,68 @@ const sizes = {
fitContent: "fit-content",
};

const calculateWidth = (margin, size) => {
if (size === "fillParent") {
return `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`;
}
return sizes[size];
};
const calculateWidth = (margin: CheckboxPropsType["margin"], size: CheckboxPropsType["size"]) =>
size === "fillParent"
? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`
: sizes[size];

const getDisabledColor = (props, element) => {
const getDisabledColor = (props, element: string) => {
switch (element) {
case "check":
return props.backgroundType && props.backgroundType === "dark"
? props.theme.disabledCheckColorOnDark
: props.theme.disabledCheckColor;
return props.backgroundType === "dark" ? props.theme.disabledCheckColorOnDark : props.theme.disabledCheckColor;
case "background":
return props.backgroundType && props.backgroundType === "dark"
return props.backgroundType === "dark"
? props.theme.disabledBackgroundColorCheckedOnDark
: props.theme.disabledBackgroundColorChecked;
case "border":
return props.backgroundType && props.backgroundType === "dark"
? props.theme.disabledBorderColorOnDark
: props.theme.disabledBorderColor;
return props.backgroundType === "dark" ? props.theme.disabledBorderColorOnDark : props.theme.disabledBorderColor;
case "label":
return props.backgroundType && props.backgroundType === "dark"
? props.theme.disabledFontColorOnDark
: props.theme.disabledFontColor;
return props.backgroundType === "dark" ? props.theme.disabledFontColorOnDark : props.theme.disabledFontColor;
}
};

const getEnabledColor = (props, element) => {
const getReadOnlyColor = (props, element: string) => {
switch (element) {
case "check":
return props.backgroundType && props.backgroundType === "dark"
? props.theme.checkColorOnDark
: props.theme.checkColor;
return props.theme.readOnlyCheckColor;
case "background":
return props.backgroundType && props.backgroundType === "dark"
return props.theme.readOnlyBackgroundColorChecked;
case "hoverBackground":
return props.theme.hoverReadOnlyBackgroundColorChecked;
case "border":
return props.theme.readOnlyBorderColor;
case "hoverBorder":
return props.theme.hoverReadOnlyBorderColor;
}
};

const getEnabledColor = (props, element: string) => {
switch (element) {
case "check":
return props.backgroundType === "dark" ? props.theme.checkColorOnDark : props.theme.checkColor;
case "background":
return props.backgroundType === "dark"
? props.theme.backgroundColorCheckedOnDark
: props.theme.backgroundColorChecked;
case "hoverBackground":
return props.backgroundType && props.backgroundType === "dark"
return props.backgroundType === "dark"
? props.theme.hoverBackgroundColorCheckedOnDark
: props.theme.hoverBackgroundColorChecked;
case "border":
return props.backgroundType && props.backgroundType === "dark"
? props.theme.borderColorOnDark
: props.theme.borderColor;
return props.backgroundType === "dark" ? props.theme.borderColorOnDark : props.theme.borderColor;
case "hoverBorder":
return props.backgroundType && props.backgroundType === "dark"
? props.theme.hoverBorderColorOnDark
: props.theme.hoverBorderColor;
return props.backgroundType === "dark" ? props.theme.hoverBorderColorOnDark : props.theme.hoverBorderColor;
case "label":
return props.backgroundType && props.backgroundType === "dark"
? props.theme.fontColorOnDark
: props.theme.fontColor;
return props.backgroundType === "dark" ? props.theme.fontColorOnDark : props.theme.fontColor;
}
};

const LabelContainer = styled.span<{ disabled: CheckboxPropsType["disabled"]; backgroundType: BackgroundColors }>`
const LabelContainer = styled.span<{
disabled: CheckboxPropsType["disabled"];
backgroundType: BackgroundColors;
labelPosition: CheckboxPropsType["labelPosition"];
}>`
order: ${(props) => (props.labelPosition === "before" ? 0 : 1)};
color: ${(props) => (props.disabled ? getDisabledColor(props, "label") : getEnabledColor(props, "label"))};
font-family: ${(props) => props.theme.fontFamily};
font-size: ${(props) => props.theme.fontSize};
Expand All @@ -198,6 +207,7 @@ const CheckboxContainer = styled.span`
const Checkbox = styled.span<{
checked: CheckboxPropsType["checked"];
disabled: CheckboxPropsType["disabled"];
readOnly: CheckboxPropsType["readOnly"];
backgroundType: BackgroundColors;
}>`
position: relative;
Expand All @@ -208,15 +218,27 @@ const Checkbox = styled.span<{
height: 18px;
width: 18px;
border: 2px solid
${(props) => (props.disabled ? getDisabledColor(props, "border") : getEnabledColor(props, "border"))};
${(props) =>
props.disabled
? getDisabledColor(props, "border")
: props.readOnly
? getReadOnlyColor(props, "border")
: getEnabledColor(props, "border")};
border-radius: 2px;
background-color: ${(props) =>
props.checked
? props.disabled
? getDisabledColor(props, "check")
: props.readOnly
? getReadOnlyColor(props, "check")
: getEnabledColor(props, "check")
: "transparent"};
color: ${(props) => (props.disabled ? getDisabledColor(props, "background") : getEnabledColor(props, "background"))};
color: ${(props) =>
props.disabled
? getDisabledColor(props, "background")
: props.readOnly
? getReadOnlyColor(props, "background")
: getEnabledColor(props, "background")};
&:focus {
outline: 2px solid
Expand All @@ -235,6 +257,7 @@ const MainContainer = styled.div<{
margin: CheckboxPropsType["margin"];
size: CheckboxPropsType["size"];
disabled: CheckboxPropsType["disabled"];
readOnly: CheckboxPropsType["readOnly"];
checked: CheckboxPropsType["checked"];
backgroundType: BackgroundColors;
}>`
Expand All @@ -251,19 +274,18 @@ const MainContainer = styled.div<{
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
margin-left: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
cursor: ${(props) => (props.disabled ? "not-allowed" : props.readOnly ? "default" : "pointer")};
&:hover ${Checkbox} {
border: 2px solid
${(props) => (props.disabled ? getDisabledColor(props, "border") : getEnabledColor(props, "hoverBorder"))};
background-color: ${(props) =>
props.checked
? props.disabled
? getDisabledColor(props, "check")
: getEnabledColor(props, "check")
: "transparent"};
color: ${(props) =>
props.disabled ? getDisabledColor(props, "background") : getEnabledColor(props, "hoverBackground")};
${(props) => {
if (!props.disabled)
return props.readOnly ? getReadOnlyColor(props, "hoverBorder") : getEnabledColor(props, "hoverBorder");
}};
color: ${(props) => {
if (!props.disabled)
return props.readOnly ? getReadOnlyColor(props, "hoverBackground") : getEnabledColor(props, "hoverBackground");
}};
}
`;

Expand Down
4 changes: 4 additions & 0 deletions lib/src/checkbox/types.ts
Expand Up @@ -41,6 +41,10 @@ type Props = {
* If true, the component will display '(Optional)' next to the label.
*/
optional?: boolean;
/**
* If true, the component will not be mutable, meaning the user can not edit the control.
*/
readOnly?: boolean;
/**
* This function will be called when the user clicks the checkbox.
* The new value will be passed as a parameter.
Expand Down
5 changes: 5 additions & 0 deletions lib/src/common/variables.ts
Expand Up @@ -199,16 +199,21 @@ export const componentTokens = {
hoverBackgroundColorCheckedOnDark: CORE_TOKENS.color_white,
disabledBackgroundColorChecked: CORE_TOKENS.color_grey_500,
disabledBackgroundColorCheckedOnDark: CORE_TOKENS.color_grey_800,
readOnlyBackgroundColorChecked: CORE_TOKENS.color_grey_500,
hoverReadOnlyBackgroundColorChecked: CORE_TOKENS.color_grey_600,
borderColor: CORE_TOKENS.color_blue_800,
borderColorOnDark: CORE_TOKENS.color_grey_200,
hoverBorderColor: CORE_TOKENS.color_blue_900,
hoverBorderColorOnDark: CORE_TOKENS.color_white,
disabledBorderColor: CORE_TOKENS.color_grey_500,
disabledBorderColorOnDark: CORE_TOKENS.color_grey_800,
readOnlyBorderColor: CORE_TOKENS.color_grey_500,
hoverReadOnlyBorderColor: CORE_TOKENS.color_grey_600,
checkColor: CORE_TOKENS.color_white,
checkColorOnDark: CORE_TOKENS.color_black,
disabledCheckColor: CORE_TOKENS.color_white,
disabledCheckColorOnDark: CORE_TOKENS.color_grey_500,
readOnlyCheckColor: CORE_TOKENS.color_white,
fontFamily: CORE_TOKENS.type_sans,
fontSize: CORE_TOKENS.type_scale_02,
fontWeight: CORE_TOKENS.type_regular,
Expand Down
15 changes: 15 additions & 0 deletions website/screens/components/checkbox/code/CheckboxCodePage.tsx
Expand Up @@ -7,6 +7,7 @@ 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 Expand Up @@ -84,6 +85,20 @@ const sections = [
to the label.
</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>onChange: function</td>
<td></td>
Expand Down

0 comments on commit 752ca0a

Please sign in to comment.