diff --git a/packages/react/src/components/checkbox-group/checkbox-group.test.tsx b/packages/react/src/components/checkbox-group/checkbox-group.test.tsx
index 3e9012dc1..72dae52fe 100644
--- a/packages/react/src/components/checkbox-group/checkbox-group.test.tsx
+++ b/packages/react/src/components/checkbox-group/checkbox-group.test.tsx
@@ -45,4 +45,39 @@ describe('Checkbox', () => {
expect(tree).toMatchSnapshot();
});
+
+ test('should display warning message if input is invalid and not checked', () => {
+ const wrapper = mountWithTheme(
+ ,
+ );
+
+ const warning = wrapper.find('div#checkbox-group-test_validationAlert');
+ expect(warning).toBeDefined();
+ });
+
+ test('should hide warning message if group is invalid and one input is checked', () => {
+ const wrapper = mountWithTheme(
+ ,
+ );
+
+ const warningPreCheck = wrapper.find('div#checkbox-group-test_validationAlert');
+ expect(warningPreCheck).toBeDefined();
+
+ wrapper.find('input').at(0).simulate('change');
+
+ const warningPostCheck = wrapper.find('div#checkbox-group-test_validationAlert');
+ expect(warningPostCheck).toEqual({});
+ });
});
diff --git a/packages/react/src/components/checkbox-group/checkbox-group.test.tsx.snap b/packages/react/src/components/checkbox-group/checkbox-group.test.tsx.snap
index 06fbc6118..4baaf08d7 100644
--- a/packages/react/src/components/checkbox-group/checkbox-group.test.tsx.snap
+++ b/packages/react/src/components/checkbox-group/checkbox-group.test.tsx.snap
@@ -114,6 +114,8 @@ exports[`Checkbox Matches the snapshot 1`] = `
class="c0"
>
`
+ align-items: flex-start;
+ color: ${({ theme }) => theme.component['checkbox-error-border-color']};
+ display: flex;
+ margin: ${({ label }) => `${label ? 'calc(var(--spacing-1x) * -1) ' : '0'} 0 0 var(--spacing-1x) `};
+ padding-bottom: var(--spacing-1x);
+`;
+
interface CheckboxProps {
+ id?: string;
label?: string;
checkedValues?: string[];
+ required?: boolean;
+ valid?: boolean;
+ validationErrorMessage?: string;
checkboxGroup: {
label: string,
name: string,
@@ -25,35 +47,67 @@ interface CheckboxProps {
}
export const CheckboxGroup: VoidFunctionComponent = ({
+ id,
label,
checkedValues,
checkboxGroup,
+ required,
+ valid = true,
+ validationErrorMessage,
onChange,
...props
}) => {
+ const { isMobile } = useDeviceContext();
+ const { t } = useTranslation('checkbox');
const dataAttributes = useDataAttributes(props);
const dataTestId = dataAttributes['data-testid'] ?? 'checkboxGroup';
+ const validationAlertId = `${id || uuid()}_validationAlert`;
+ const [checkedState, setCheckedState] = useState(
+ new Array(checkboxGroup.length).fill(false),
+ );
+ const areAllCheckboxUnchecked = checkedState.every((e) => e === false);
+
+ const handleOnChange = (position: number): void => {
+ const updatedCheckedState = checkedState.map((item, index) => (index === position ? !item : item));
+
+ setCheckedState(updatedCheckedState);
+ };
return (
<>
{label && }
+ {
+ required && !valid && areAllCheckboxUnchecked
+ && (
+
+
+ {validationErrorMessage || t('validationErrorMessage')}
+
+ )
+ }
{checkboxGroup.map(({
defaultChecked,
disabled,
label: checkboxLabel,
name,
value,
- }) => (
+ }, pos) => (
) => {
+ handleOnChange(pos);
+ onChange?.(event);
+ }}
/>
))}
>
diff --git a/packages/react/src/components/checkbox/checkbox.test.tsx b/packages/react/src/components/checkbox/checkbox.test.tsx
index 5869e6800..a3cd4725a 100644
--- a/packages/react/src/components/checkbox/checkbox.test.tsx
+++ b/packages/react/src/components/checkbox/checkbox.test.tsx
@@ -49,6 +49,60 @@ describe('Checkbox', () => {
expect(input.prop('disabled')).toBe(true);
});
+ test('should be required when required prop is set to true', () => {
+ const wrapper = mountWithTheme();
+
+ const input = wrapper.find('input');
+ expect(input.prop('required')).toBe(true);
+ });
+
+ test('should display warning message if input is invalid and not checked', () => {
+ const wrapper = mountWithTheme();
+
+ const warning = wrapper.find('div#checkbox-test_validationAlert');
+ expect(warning).toBeDefined();
+ });
+
+ test('should hide warning message if input is invalid and then checked', () => {
+ const wrapper = mountWithTheme();
+
+ const warningPreCheck = wrapper.find('div#checkbox-test_validationAlert');
+ expect(warningPreCheck).toBeDefined();
+
+ wrapper.find('input').simulate('change');
+
+ const warningPostCheck = wrapper.find('div#checkbox-test_validationAlert');
+ expect(warningPostCheck).toEqual({});
+ });
+
+ test('should have aria-labelledby prop when warning message is displayed and input is invalid', () => {
+ const wrapper = mountWithTheme();
+
+ const input = wrapper.find('input');
+ expect(input.prop('aria-labelledby')).toBe('checkbox-test_validationAlert');
+ });
+
+ test('should have empty aria-labelledby prop when input is valid', () => {
+ const wrapper = mountWithTheme();
+
+ const input = wrapper.find('input');
+ expect(input.prop('aria-labelledby')).toBe('');
+ });
+
+ test('should have aria-invalid prop set to true when warning message is displayed and input is invalid', () => {
+ const wrapper = mountWithTheme();
+
+ const input = wrapper.find('input');
+ expect(input.prop('aria-invalid')).toBe(true);
+ });
+
+ test('should have aria-invalid prop set to false when input is valid', () => {
+ const wrapper = mountWithTheme();
+
+ const input = wrapper.find('input');
+ expect(input.prop('aria-invalid')).toBe(false);
+ });
+
test('matches snapshot', () => {
const tree = mountWithTheme();
diff --git a/packages/react/src/components/checkbox/checkbox.test.tsx.snap b/packages/react/src/components/checkbox/checkbox.test.tsx.snap
index a12b39a2a..e6ec64a28 100644
--- a/packages/react/src/components/checkbox/checkbox.test.tsx.snap
+++ b/packages/react/src/components/checkbox/checkbox.test.tsx.snap
@@ -110,18 +110,26 @@ exports[`Checkbox matches snapshot 1`] = `
className="c0"
>
-
+
diff --git a/packages/react/src/components/checkbox/checkbox.tsx b/packages/react/src/components/checkbox/checkbox.tsx
index 6b6be33bf..5fbb1fac3 100644
--- a/packages/react/src/components/checkbox/checkbox.tsx
+++ b/packages/react/src/components/checkbox/checkbox.tsx
@@ -6,12 +6,16 @@ import {
Ref,
useEffect,
useImperativeHandle,
- useRef,
+ useRef, useState,
} from 'react';
import styled, { css } from 'styled-components';
import { focus } from '../../utils/css-state';
import { Icon } from '../icon/icon';
import { useDataAttributes } from '../../hooks/use-data-attributes';
+import { ResolvedTheme } from '../../themes/theme';
+import { useTranslation } from '../../i18n/use-translation';
+import { useDeviceContext } from '../device-context-provider/device-context-provider';
+import { v4 as uuid } from '../../utils/uuid';
const checkboxWidth = 'var(--size-1x)';
@@ -32,9 +36,27 @@ const IndeterminateIcon = styled(Icon).attrs({ name: 'minus' })`
${iconStyles}
`;
-const CustomCheckbox = styled.span<{ disabled?: boolean }>`
+interface CheckboxStyleProps {
+ disabled?: boolean;
+ theme: ResolvedTheme;
+ $valid: boolean;
+}
+
+function getBorderColor({ disabled, theme, $valid }: CheckboxStyleProps): string {
+ if (disabled) {
+ return theme.component['checkbox-disabled-border-color'];
+ }
+
+ if (!$valid) {
+ return theme.component['checkbox-error-border-color'];
+ }
+
+ return theme.component['checkbox-unchecked-border-color'];
+}
+
+const CustomCheckbox = styled.span`
background-color: ${({ disabled, theme }) => (disabled ? theme.component['checkbox-disabled-background-color'] : theme.component['checkbox-unchecked-background-color'])};
- border: 1px solid ${({ disabled, theme }) => (disabled ? theme.component['checkbox-disabled-border-color'] : theme.component['checkbox-unchecked-border-color'])};
+ border: 1px solid ${getBorderColor};
border-radius: var(--border-radius);
box-sizing: border-box;
display: inline-block;
@@ -95,6 +117,20 @@ const StyledLabel = styled.label`
}
`;
+const StyledIcon = styled(Icon)`
+ align-self: center;
+ display: flex;
+ margin-right: var(--spacing-base);
+`;
+
+const ValidationErrorAlert = styled.div`
+ align-items: flex-start;
+ color: ${({ theme }) => theme.component['checkbox-error-border-color']};
+ display: flex;
+ margin-left: var(--spacing-1x);
+ padding-bottom: var(--spacing-1x);
+`;
+
export interface CheckboxProps {
checked?: boolean;
className?: string;
@@ -102,8 +138,12 @@ export interface CheckboxProps {
disabled?: boolean,
id?: string,
indeterminate?: boolean;
+ isInGroup?: boolean;
label?: string,
name?: string,
+ required?: boolean,
+ valid?: boolean,
+ validationErrorMessage?: string;
value?: string,
onChange?(event: ChangeEvent): void;
@@ -116,13 +156,21 @@ export const Checkbox: FunctionComponent> = for
disabled,
id,
indeterminate,
+ isInGroup = false,
label,
name,
+ required,
+ valid = true,
+ validationErrorMessage,
value,
onChange,
...otherProps
}, ref: Ref) => {
+ const { isMobile } = useDeviceContext();
+ const { t } = useTranslation('checkbox');
const checkboxRef = useRef(null);
+ const [isChecked, setIsChecked] = useState(checked || false);
+ const validationAlertId = `${id || uuid()}_validationAlert`;
useImperativeHandle(ref, () => checkboxRef.current, [checkboxRef]);
const dataAttributes = useDataAttributes(otherProps);
@@ -130,35 +178,53 @@ export const Checkbox: FunctionComponent> = for
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = indeterminate || false;
+ checkboxRef.current.checked = checked || false;
}
- }, [indeterminate]);
+ }, [indeterminate, checked]);
return (
-
-
+ {
+ required && !valid && !isChecked && !isInGroup
+ && (
+
+
+ {validationErrorMessage || t('validationErrorMessage')}
+
+ )
+ }
+
-
-
-
-
- {label}
-
+ key={`${name}-${value}`}
+ >
+ ) => {
+ setIsChecked(!!checkboxRef.current?.checked);
+ onChange?.(event);
+ }}
+ {...dataAttributes /* eslint-disable-line react/jsx-props-no-spreading */}
+ />
+
+
+
+
+ {label}
+
+ >
);
});
diff --git a/packages/react/src/components/table/table.test.tsx.snap b/packages/react/src/components/table/table.test.tsx.snap
index fa2a1a567..285c770e0 100644
--- a/packages/react/src/components/table/table.test.tsx.snap
+++ b/packages/react/src/components/table/table.test.tsx.snap
@@ -1630,6 +1630,8 @@ exports[`Table has selectable rows styles 1`] = `
class="c4"
>
(
/>
);
+export const Required: Story = () => (
+
+);
+
export const Controlled: Story = () => (
+### Required and invalid
+
## Properties
diff --git a/packages/storybook/stories/checkbox.stories.tsx b/packages/storybook/stories/checkbox.stories.tsx
index 880799a3a..fe952a454 100644
--- a/packages/storybook/stories/checkbox.stories.tsx
+++ b/packages/storybook/stories/checkbox.stories.tsx
@@ -4,6 +4,8 @@ import { FunctionComponent } from 'react';
// eslint-disable-next-line react/jsx-props-no-spreading
export const Default: FunctionComponent = (props) => ;
+// eslint-disable-next-line react/jsx-props-no-spreading
+export const Required: FunctionComponent = (props) => ;
const CheckboxMeta: Meta = {
title: 'Components/Checkbox',