@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 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.1",
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 }];
@@ -68,6 +68,7 @@ export const CheckboxGroupField: FC<CheckboxGroupFieldProps> = ({
68
68
  }
69
69
  }}
70
70
  required={required && !disabled}
71
+ hasError={showValidationMessage}
71
72
  showValidationMessage={!leftLabels && showValidationMessage}
72
73
  validationMessage={validationMessage}
73
74
  />
@@ -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;