@cerebruminc/cerebellum 17.0.1 → 17.0.2-beta.dangerous.05ab9b5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +5 -5
- package/src/components/CheckboxGroup/CheckboxGroup.test.tsx +29 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +2 -1
- package/src/components/CheckboxGroup/types.ts +3 -0
- package/src/components/DatePicker/DatePickerStyles.tsx +3 -1
- package/src/components/Form/Form.test.tsx +28 -0
- package/src/components/Form/fields/CheckboxGroupField/CheckboxGroupField.tsx +1 -0
- package/src/components/Form/fields/RadioGroupField/RadioGroupField.tsx +1 -0
- package/src/components/Form/fields/ToggleGroupField/ToggleGroupField.tsx +1 -0
- package/src/components/InlineInput/InlineInput.tsx +1 -0
- package/src/components/Input/Input.test.tsx +28 -0
- package/src/components/RadioGroup/RadioGroup.test.tsx +29 -0
- package/src/components/RadioGroup/RadioGroup.tsx +2 -1
- package/src/components/RadioGroup/types.ts +3 -0
- package/src/components/Textarea/InlineTextarea.tsx +1 -0
- package/src/components/Textarea/Textarea.test.tsx +25 -0
- package/src/components/ToggleGroup/ToggleGroup.test.tsx +29 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +2 -1
- package/src/components/ToggleGroup/types.ts +3 -0
- package/src/sharedStyle/Inputs.tsx +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# react-component-lib-boilerplate
|
|
2
2
|
|
|
3
|
+
## [17.0.2](https://github.com/cerebruminc/cerebellum/compare/v17.0.1...v17.0.2) (2026-05-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* add types as peer deps ([c9ba834](https://github.com/cerebruminc/cerebellum/commit/c9ba834cdd481c05242afd5cfbbd384dc1c61d33))
|
|
9
|
+
|
|
3
10
|
## [17.0.1](https://github.com/cerebruminc/cerebellum/compare/v17.0.0...v17.0.1) (2026-05-18)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerebruminc/cerebellum",
|
|
3
|
-
"version": "17.0.
|
|
3
|
+
"version": "17.0.2-beta.dangerous.05ab9b5",
|
|
4
4
|
"description": "Cerebrum's React Component Library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -77,12 +77,8 @@
|
|
|
77
77
|
"@testing-library/user-event": "^14.0.0",
|
|
78
78
|
"@types/jest": "^29.5.12",
|
|
79
79
|
"@types/jscodeshift": "^0.12.0",
|
|
80
|
-
"@types/object-path": "^0.11.4",
|
|
81
|
-
"@types/pngjs": "^6.0.5",
|
|
82
80
|
"@types/react": "^18.2.66",
|
|
83
|
-
"@types/react-datepicker": "^6.0.3",
|
|
84
81
|
"@types/react-dom": "^18.2.22",
|
|
85
|
-
"@types/react-places-autocomplete": "^7.2.14",
|
|
86
82
|
"babel-jest": "^29.6.1",
|
|
87
83
|
"babel-loader": "^10.0.0",
|
|
88
84
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
|
@@ -110,6 +106,10 @@
|
|
|
110
106
|
"@mantine/core": "^8.3.16",
|
|
111
107
|
"@mantine/modals": "^8.3.16",
|
|
112
108
|
"@mantine/notifications": "^8.3.16",
|
|
109
|
+
"@types/object-path": "^0.11.4",
|
|
110
|
+
"@types/pngjs": "^6.0.5",
|
|
111
|
+
"@types/react-datepicker": "^6.0.3",
|
|
112
|
+
"@types/react-places-autocomplete": "^7.2.14",
|
|
113
113
|
"next": "^14.0.0 || ^15.0.0",
|
|
114
114
|
"react": "^18.0.0 || ^19.0.0",
|
|
115
115
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
@@ -43,6 +43,35 @@ describe("CheckboxGroup", () => {
|
|
|
43
43
|
expect(validationTextNode).toBeInTheDocument();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
47
|
+
const validationMessage = "Selection required";
|
|
48
|
+
render(
|
|
49
|
+
<ThemedCheckboxGroup onClick={jest.fn()} activeValues={[]} checkboxes={[]} label="foo" validationMessage={validationMessage} showValidationMessage />
|
|
50
|
+
);
|
|
51
|
+
const alert = screen.getByRole("alert");
|
|
52
|
+
expect(alert).toHaveTextContent(validationMessage);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
56
|
+
const validationMessage = "Selection required";
|
|
57
|
+
render(<ThemedCheckboxGroup onClick={jest.fn()} activeValues={[]} checkboxes={[]} label="foo" validationMessage={validationMessage} />);
|
|
58
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("group has aria-invalid when showValidationMessage is true", () => {
|
|
62
|
+
render(
|
|
63
|
+
<ThemedCheckboxGroup onClick={jest.fn()} activeValues={[]} checkboxes={[]} label="foo" validationMessage="Error" showValidationMessage />
|
|
64
|
+
);
|
|
65
|
+
const group = screen.getByRole("group");
|
|
66
|
+
expect(group).toHaveAttribute("aria-invalid", "true");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("group does not have aria-invalid when showValidationMessage is false", () => {
|
|
70
|
+
render(<ThemedCheckboxGroup onClick={jest.fn()} activeValues={[]} checkboxes={[]} label="foo" />);
|
|
71
|
+
const group = screen.getByRole("group");
|
|
72
|
+
expect(group).not.toHaveAttribute("aria-invalid");
|
|
73
|
+
});
|
|
74
|
+
|
|
46
75
|
test("renders multiple checkboxes text", () => {
|
|
47
76
|
const text = "Multiple CheckboxGroup text";
|
|
48
77
|
const labels = ["Test label 1", "Test label 2", "Test label 3"];
|
|
@@ -34,6 +34,7 @@ export const CheckboxGroup: FC<CheckboxGroupType> = ({
|
|
|
34
34
|
noBottomPadding = false,
|
|
35
35
|
onClick,
|
|
36
36
|
required,
|
|
37
|
+
hasError,
|
|
37
38
|
showValidationMessage = false,
|
|
38
39
|
titleColor,
|
|
39
40
|
validationMessage,
|
|
@@ -44,7 +45,7 @@ export const CheckboxGroup: FC<CheckboxGroupType> = ({
|
|
|
44
45
|
const disabledTextColor = disabledLabelColor || theme.text.disabledColor;
|
|
45
46
|
|
|
46
47
|
return (
|
|
47
|
-
<CheckboxGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={checkboxGroupWidth} role="group" aria-required={required ? "true" : undefined}>
|
|
48
|
+
<CheckboxGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={checkboxGroupWidth} role="group" aria-invalid={(hasError ?? showValidationMessage) ? "true" : undefined} aria-required={required ? "true" : undefined}>
|
|
48
49
|
{label ? (
|
|
49
50
|
typeof label === "string" ? (
|
|
50
51
|
<BodyXSEmphasis $textColor={disableGroup ? disabledTextColor : titleColor} data-sentry-unmask>
|
|
@@ -31,6 +31,9 @@ export interface CheckboxGroupType extends Omit<CheckboxItemType, "active" | "di
|
|
|
31
31
|
onClick: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
32
32
|
/** Adds an asterisk to the UI when there is also a `label` */
|
|
33
33
|
required?: boolean;
|
|
34
|
+
/** Sets aria-invalid on the group independently of showValidationMessage. Useful when the inline
|
|
35
|
+
* message is suppressed (e.g. leftLabels Form layout) but the field is still in an error state. */
|
|
36
|
+
hasError?: boolean;
|
|
34
37
|
/** Displays an error message under the last checkbox */
|
|
35
38
|
showValidationMessage?: boolean;
|
|
36
39
|
/** The title color. THEME PROP: default is colors.COOL_GREY_80 */
|
|
@@ -119,7 +119,9 @@ export const HelperText = styled.p<HelperTextProps>`
|
|
|
119
119
|
font-weight: 400;
|
|
120
120
|
margin-top: 10px;
|
|
121
121
|
`;
|
|
122
|
-
export const ValidationText = styled.p<ValidationTextProps
|
|
122
|
+
export const ValidationText = styled.p.attrs<ValidationTextProps>(({ $showValidationMessage }) => ({
|
|
123
|
+
role: $showValidationMessage ? "alert" : undefined,
|
|
124
|
+
}))<ValidationTextProps>`
|
|
123
125
|
color: ${(props) =>
|
|
124
126
|
props.disabled ? props.$disabledColor || props.theme.datePicker?.disabledColor : props.$failColor || props.theme.datePicker?.failColor};
|
|
125
127
|
display: inline-block;
|
|
@@ -144,6 +144,34 @@ describe("Form", () => {
|
|
|
144
144
|
await waitFor(() => expect(screen.getByText(errorMessage)).toBeInTheDocument());
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
test("validation error has role='alert' for screen reader announcement", async () => {
|
|
148
|
+
const errorMessage = "This field is required";
|
|
149
|
+
const submitButtonText = "Submit";
|
|
150
|
+
const fields: SingleOrMultipleFieldType[] = [{ fieldLabel: "field 1", required: true, requiredMessage: errorMessage, name: "field 1" }];
|
|
151
|
+
render(<ThemedForm submitButtonText={submitButtonText} title="foo" fields={fields} submit={jest.fn()} />);
|
|
152
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
153
|
+
const button = screen.getByText(submitButtonText);
|
|
154
|
+
await waitFor(() => userEvent.click(button));
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
const alert = screen.getByRole("alert");
|
|
157
|
+
expect(alert).toHaveTextContent(errorMessage);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("leftLabels validation error has role='alert' for screen reader announcement", async () => {
|
|
162
|
+
const errorMessage = "This field is required";
|
|
163
|
+
const submitButtonText = "Submit";
|
|
164
|
+
const fields: SingleOrMultipleFieldType[] = [{ fieldLabel: "field 1", required: true, requiredMessage: errorMessage, name: "field 1" }];
|
|
165
|
+
render(<ThemedForm leftLabels submitButtonText={submitButtonText} title="foo" fields={fields} submit={jest.fn()} />);
|
|
166
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
167
|
+
const button = screen.getByText(submitButtonText);
|
|
168
|
+
await waitFor(() => userEvent.click(button));
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
const alert = screen.getByRole("alert");
|
|
171
|
+
expect(alert).toHaveTextContent(errorMessage);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
147
175
|
test("disableSubmitUntilValid disables submit button if the form is not completed", async () => {
|
|
148
176
|
const onSubmit = jest.fn();
|
|
149
177
|
const fields = [{ fieldLabel: "field 1", name: "bar", required: true }];
|
|
@@ -69,6 +69,7 @@ export const RadioGroupField: FC<RadioGroupFieldProps> = ({
|
|
|
69
69
|
}}
|
|
70
70
|
radioLabel={leftLabels ? undefined : fieldLabel || name}
|
|
71
71
|
required={required && !disabled}
|
|
72
|
+
hasError={showValidationMessage}
|
|
72
73
|
showValidationMessage={!leftLabels && showValidationMessage}
|
|
73
74
|
validationMessage={validationMessage}
|
|
74
75
|
/>
|
|
@@ -68,6 +68,7 @@ export const ToggleGroupField: FC<ToggleGroupFieldProps> = ({
|
|
|
68
68
|
}
|
|
69
69
|
}}
|
|
70
70
|
required={required && !disabled}
|
|
71
|
+
hasError={showValidationMessage}
|
|
71
72
|
showValidationMessage={!leftLabels && showValidationMessage}
|
|
72
73
|
toggleGroupLabel={leftLabels ? undefined : fieldLabel || name}
|
|
73
74
|
validationMessage={validationMessage}
|
|
@@ -191,6 +191,7 @@ export const InlineInput: FC<InlineInputType> = ({
|
|
|
191
191
|
$showValidationMessage={showValidationMessage}
|
|
192
192
|
$sidePadding={sidePadding}
|
|
193
193
|
step={type === "number" ? step : undefined}
|
|
194
|
+
aria-invalid={showValidationMessage ? "true" : undefined}
|
|
194
195
|
aria-required={required ? "true" : undefined}
|
|
195
196
|
required={required}
|
|
196
197
|
role="textbox"
|
|
@@ -43,6 +43,34 @@ describe("Input", () => {
|
|
|
43
43
|
expect(asterisk).toBeInTheDocument();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
// ------ Screen reader accessibility tests ------
|
|
47
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
48
|
+
const validationMessage = "This field is required";
|
|
49
|
+
render(<ThemedInput value="" validationMessage={validationMessage} showValidationMessage />);
|
|
50
|
+
const alert = screen.getByRole("alert");
|
|
51
|
+
expect(alert).toHaveTextContent(validationMessage);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
55
|
+
const validationMessage = "This field is required";
|
|
56
|
+
render(<ThemedInput value="" validationMessage={validationMessage} />);
|
|
57
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("input has aria-invalid when showValidationMessage is true", () => {
|
|
61
|
+
const label = "Test input";
|
|
62
|
+
render(<ThemedInput value="" inputLabel={label} showValidationMessage validationMessage="Error" />);
|
|
63
|
+
const input = screen.getByLabelText(label);
|
|
64
|
+
expect(input).toHaveAttribute("aria-invalid", "true");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("input does not have aria-invalid when showValidationMessage is false", () => {
|
|
68
|
+
const label = "Test input";
|
|
69
|
+
render(<ThemedInput value="" inputLabel={label} />);
|
|
70
|
+
const input = screen.getByLabelText(label);
|
|
71
|
+
expect(input).not.toHaveAttribute("aria-invalid");
|
|
72
|
+
});
|
|
73
|
+
|
|
46
74
|
// ------ The rest of these tests are redundant with InlinInput's tests, but there's not much point deleting them
|
|
47
75
|
test("renders Input with the text present", () => {
|
|
48
76
|
const inputValue = "foo";
|
|
@@ -47,6 +47,35 @@ describe("RadioGroup", () => {
|
|
|
47
47
|
expect(validationMessage).toBeInTheDocument();
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
51
|
+
const text = "Selection required";
|
|
52
|
+
render(
|
|
53
|
+
<ThemedRadioGroup activeId="" onRadioClick={jest.fn()} radios={radioExamples} showValidationMessage validationMessage={text} name="test_radio" />
|
|
54
|
+
);
|
|
55
|
+
const alert = screen.getByRole("alert");
|
|
56
|
+
expect(alert).toHaveTextContent(text);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
60
|
+
const text = "Selection required";
|
|
61
|
+
render(<ThemedRadioGroup activeId="" onRadioClick={jest.fn()} radios={radioExamples} validationMessage={text} name="test_radio" />);
|
|
62
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("radiogroup has aria-invalid when showValidationMessage is true", () => {
|
|
66
|
+
render(
|
|
67
|
+
<ThemedRadioGroup activeId="" onRadioClick={jest.fn()} radios={radioExamples} showValidationMessage validationMessage="Error" name="test_radio" />
|
|
68
|
+
);
|
|
69
|
+
const group = screen.getByRole("radiogroup");
|
|
70
|
+
expect(group).toHaveAttribute("aria-invalid", "true");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("radiogroup does not have aria-invalid when showValidationMessage is false", () => {
|
|
74
|
+
render(<ThemedRadioGroup activeId="" onRadioClick={jest.fn()} radios={radioExamples} name="test_radio" />);
|
|
75
|
+
const group = screen.getByRole("radiogroup");
|
|
76
|
+
expect(group).not.toHaveAttribute("aria-invalid");
|
|
77
|
+
});
|
|
78
|
+
|
|
50
79
|
test("multiple radios test", () => {
|
|
51
80
|
const text = "Multiple RadioGroup text";
|
|
52
81
|
const onClick = jest.fn();
|
|
@@ -34,6 +34,7 @@ export const RadioGroup: FC<RadioGroupType> = ({
|
|
|
34
34
|
radioSize,
|
|
35
35
|
radios,
|
|
36
36
|
required,
|
|
37
|
+
hasError,
|
|
37
38
|
showValidationMessage = false,
|
|
38
39
|
titleColor,
|
|
39
40
|
labelGap,
|
|
@@ -57,7 +58,7 @@ export const RadioGroup: FC<RadioGroupType> = ({
|
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
return (
|
|
60
|
-
<RadioGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={radioGroupWidth} role="radiogroup" aria-required={required ? "true" : undefined}>
|
|
61
|
+
<RadioGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={radioGroupWidth} role="radiogroup" aria-invalid={(hasError ?? showValidationMessage) ? "true" : undefined} aria-required={required ? "true" : undefined}>
|
|
61
62
|
<LabelBox>
|
|
62
63
|
{showLabel()}
|
|
63
64
|
{required && <Asterisk $asteriskColor={asteriskColor}>*</Asterisk>}
|
|
@@ -33,6 +33,9 @@ export interface RadioGroupType extends Omit<RadioItemType, "active" | "disabled
|
|
|
33
33
|
radioGroupWidth?: number;
|
|
34
34
|
/** Adds an asterisk to the UI when there is also a `radioLabel` */
|
|
35
35
|
required?: boolean;
|
|
36
|
+
/** Sets aria-invalid on the group independently of showValidationMessage. Useful when the inline
|
|
37
|
+
* message is suppressed (e.g. leftLabels Form layout) but the field is still in an error state. */
|
|
38
|
+
hasError?: boolean;
|
|
36
39
|
/** Displays an error message under the last radio */
|
|
37
40
|
showValidationMessage?: boolean;
|
|
38
41
|
/** The title color. THEME PROP: default is colors.COOL_GREY_80 */
|
|
@@ -37,6 +37,7 @@ export const InlineTextarea: FC<InlineTextareaType> = ({
|
|
|
37
37
|
return (
|
|
38
38
|
<PositionWrapper $width={inputWidth}>
|
|
39
39
|
<TextareaElement
|
|
40
|
+
aria-invalid={showValidationMessage ? "true" : undefined}
|
|
40
41
|
id={htmlId}
|
|
41
42
|
ref={textareaRef}
|
|
42
43
|
$activeBorderColor={activeBorderColor}
|
|
@@ -78,6 +78,31 @@ describe("Textarea", () => {
|
|
|
78
78
|
expect(element).toBeInTheDocument();
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
82
|
+
const validationMessage = "This field is required";
|
|
83
|
+
render(<ThemedTextarea showValidationMessage validationMessage={validationMessage} value={"Foo"} />);
|
|
84
|
+
const alert = screen.getByRole("alert");
|
|
85
|
+
expect(alert).toHaveTextContent(validationMessage);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
89
|
+
const validationMessage = "This field is required";
|
|
90
|
+
render(<ThemedTextarea validationMessage={validationMessage} value={"Foo"} />);
|
|
91
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("textarea has aria-invalid when showValidationMessage is true", () => {
|
|
95
|
+
render(<ThemedTextarea showValidationMessage validationMessage="Error" value={"Foo"} />);
|
|
96
|
+
const textarea = screen.getByRole("textbox");
|
|
97
|
+
expect(textarea).toHaveAttribute("aria-invalid", "true");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("textarea does not have aria-invalid when showValidationMessage is false", () => {
|
|
101
|
+
render(<ThemedTextarea value={"Foo"} />);
|
|
102
|
+
const textarea = screen.getByRole("textbox");
|
|
103
|
+
expect(textarea).not.toHaveAttribute("aria-invalid");
|
|
104
|
+
});
|
|
105
|
+
|
|
81
106
|
test("helper text is visible when received as a prop", () => {
|
|
82
107
|
const helperText = "This is a helper text";
|
|
83
108
|
render(<ThemedTextarea helperText={helperText} value={"Foo"} />);
|
|
@@ -35,6 +35,35 @@ describe("ToggleGroup", () => {
|
|
|
35
35
|
expect(errorMessage).toBeInTheDocument();
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
39
|
+
const validationMessage = "Selection required";
|
|
40
|
+
render(
|
|
41
|
+
<ThemedToggleGroup showValidationMessage validationMessage={validationMessage} onToggle={jest.fn()} toggles={[]} activeValues={[]} />
|
|
42
|
+
);
|
|
43
|
+
const alert = screen.getByRole("alert");
|
|
44
|
+
expect(alert).toHaveTextContent(validationMessage);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
48
|
+
const validationMessage = "Selection required";
|
|
49
|
+
render(<ThemedToggleGroup validationMessage={validationMessage} onToggle={jest.fn()} toggles={[]} activeValues={[]} />);
|
|
50
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("group has aria-invalid when showValidationMessage is true", () => {
|
|
54
|
+
render(
|
|
55
|
+
<ThemedToggleGroup showValidationMessage validationMessage="Error" onToggle={jest.fn()} toggles={[]} activeValues={[]} />
|
|
56
|
+
);
|
|
57
|
+
const group = screen.getByRole("group");
|
|
58
|
+
expect(group).toHaveAttribute("aria-invalid", "true");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("group does not have aria-invalid when showValidationMessage is false", () => {
|
|
62
|
+
render(<ThemedToggleGroup toggleGroupLabel="label" onToggle={jest.fn()} toggles={[]} activeValues={[]} />);
|
|
63
|
+
const group = screen.getByRole("group");
|
|
64
|
+
expect(group).not.toHaveAttribute("aria-invalid");
|
|
65
|
+
});
|
|
66
|
+
|
|
38
67
|
test("multiple toggles test", () => {
|
|
39
68
|
const labels = ["Test label 1", "Test label 2", "Test label 3"];
|
|
40
69
|
const onToggle = jest.fn();
|
|
@@ -23,6 +23,7 @@ export const ToggleGroup: FC<ToggleGroupType> = (props) => {
|
|
|
23
23
|
noBottomPadding = false,
|
|
24
24
|
onToggle,
|
|
25
25
|
required,
|
|
26
|
+
hasError,
|
|
26
27
|
showValidationMessage = false,
|
|
27
28
|
toggles,
|
|
28
29
|
titleColor,
|
|
@@ -35,7 +36,7 @@ export const ToggleGroup: FC<ToggleGroupType> = (props) => {
|
|
|
35
36
|
const disabledTextColor = disabledLabelColor || theme.text.disabledColor;
|
|
36
37
|
|
|
37
38
|
return (
|
|
38
|
-
<ToggleGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={toggleGroupWidth} role="group" aria-required={required ? "true" : undefined}>
|
|
39
|
+
<ToggleGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={toggleGroupWidth} role="group" aria-invalid={(hasError ?? showValidationMessage) ? "true" : undefined} aria-required={required ? "true" : undefined}>
|
|
39
40
|
{toggleGroupLabel ? (
|
|
40
41
|
typeof toggleGroupLabel === "string" ? (
|
|
41
42
|
<BodyXSEmphasis $textColor={disableGroup ? disabledTextColor : titleColor} data-sentry-unmask>
|
|
@@ -20,6 +20,9 @@ export interface ToggleGroupType extends Omit<ToggleItemType, "active" | "label"
|
|
|
20
20
|
onToggle?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
21
21
|
/** Adds an asterisk to the UI when there is also a `toggleGroupLabel` */
|
|
22
22
|
required?: boolean;
|
|
23
|
+
/** Sets aria-invalid on the group independently of showValidationMessage. Useful when the inline
|
|
24
|
+
* message is suppressed (e.g. leftLabels Form layout) but the field is still in an error state. */
|
|
25
|
+
hasError?: boolean;
|
|
23
26
|
/** Displays an error message under the last toggle */
|
|
24
27
|
showValidationMessage?: boolean;
|
|
25
28
|
/** The label for ToggleGroup. Not required, but strongly recommended. */
|
|
@@ -221,7 +221,9 @@ export const TrailingText = styled.p<TrailingTextProps>`
|
|
|
221
221
|
right: ${({ $iconSpaceAfter, $passwordManagerPresent }) => ($iconSpaceAfter ? 45 : 16) + ($passwordManagerPresent ? pwWidth : 0)}px;
|
|
222
222
|
`;
|
|
223
223
|
|
|
224
|
-
export const ValidationText = styled.p<ValidationTextProps
|
|
224
|
+
export const ValidationText = styled.p.attrs<ValidationTextProps>(({ $showValidationMessage }) => ({
|
|
225
|
+
role: $showValidationMessage ? "alert" : undefined,
|
|
226
|
+
}))<ValidationTextProps>`
|
|
225
227
|
color: ${({ $disabled, $disabledColor, theme, $failColor: failColor }) =>
|
|
226
228
|
$disabled ? $disabledColor || theme.input?.disabledColor : failColor || theme.input?.failColor};
|
|
227
229
|
font-size: 14px;
|