@cerebruminc/cerebellum 17.2.1 → 17.3.1

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,28 @@
1
1
  # react-component-lib-boilerplate
2
2
 
3
+ ## [17.3.1](https://github.com/cerebruminc/cerebellum/compare/v17.3.0...v17.3.1) (2026-06-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **switches:** make switches focus state accessible ([48009b6](https://github.com/cerebruminc/cerebellum/commit/48009b674b71a15ee0a17d65c54f01ec6033fea1))
9
+ * **toggle:** make Toggle focus state accessible ([3f6623d](https://github.com/cerebruminc/cerebellum/commit/3f6623d2d8c7489279e90312858d066bee759044))
10
+ * **toggle:** revert knobShadow box-shadow ([281c3e4](https://github.com/cerebruminc/cerebellum/commit/281c3e41b7f32fdca55267dfccf5126322d59db6))
11
+
12
+ ## [17.3.0](https://github.com/cerebruminc/cerebellum/compare/v17.2.1...v17.3.0) (2026-06-09)
13
+
14
+
15
+ ### Features
16
+
17
+ * **ci:** move cerebellum to use DRY actions ([f2693cc](https://github.com/cerebruminc/cerebellum/commit/f2693ccaa773ac106ec4465f9973f3c0b1759f52))
18
+ * **rjsform:** add buttonShadow to RJSForm ([fceb013](https://github.com/cerebruminc/cerebellum/commit/fceb0136becc142921fe7fd23b8b524887bacfb4))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **form:** convert empty titles to div ([89eaeba](https://github.com/cerebruminc/cerebellum/commit/89eaebab7900b747f9cdd682ce24cf33d8367053))
24
+ * **image-picker:** make FakeTextLink keyboard-focusable for file picker ([16b2ad3](https://github.com/cerebruminc/cerebellum/commit/16b2ad3dea861c93cd207bc080217e8914b1fe04))
25
+
3
26
  ## [17.2.1](https://github.com/cerebruminc/cerebellum/compare/v17.2.0...v17.2.1) (2026-06-02)
4
27
 
5
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerebruminc/cerebellum",
3
- "version": "17.2.1",
3
+ "version": "17.3.1",
4
4
  "description": "Cerebrum's React Component Library",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -13,7 +13,7 @@ export const CheckContainer = styled.span<CheckContainerProps>`
13
13
  z-index: 0;
14
14
  &:after {
15
15
  content: "";
16
- border: 7px solid ${({ $focusedColor, theme }) => $focusedColor || theme.switch?.focusedBorderColor};
16
+ border: 1px solid ${({ $focusedColor, theme }) => $focusedColor || theme.switch?.focusedBorderColor};
17
17
  border-radius: calc(2px + 10.5%);
18
18
  top: -5px;
19
19
  bottom: -5px;
@@ -23,7 +23,7 @@ export const Option = styled.div<OptionProps>`
23
23
  position: relative;
24
24
  ${RadioContainer}:after {
25
25
  content: "";
26
- border: 7px solid ${({ $themeOverride, theme }) => $themeOverride?.focusedColor || theme.switch?.focusedBorderColor};
26
+ border: 1px solid ${({ $themeOverride, theme }) => $themeOverride?.focusedColor || theme.switch?.focusedBorderColor};
27
27
  border-radius: 50%;
28
28
  top: -5px;
29
29
  bottom: -5px;
@@ -36,7 +36,7 @@ export const Option = styled.div<OptionProps>`
36
36
  }
37
37
  ${CheckContainer}:after {
38
38
  content: "";
39
- border: 7px solid ${({ $themeOverride, theme }) => $themeOverride?.focusedColor || theme.switch?.focusedBorderColor};
39
+ border: 1px solid ${({ $themeOverride, theme }) => $themeOverride?.focusedColor || theme.switch?.focusedBorderColor};
40
40
  border-radius: calc(2px + 10.5%);
41
41
  top: -5px;
42
42
  bottom: -5px;
@@ -203,7 +203,7 @@ export const Form = <T extends Record<string, any>>({
203
203
  {/* children are not expected in Form, but are needed to get FormSubmitWatcher to work */}
204
204
  {children}
205
205
  <FormTitleGroup>
206
- <Title $themeOverride={themeOverride} data-sentry-unmask>
206
+ <Title $themeOverride={themeOverride} data-sentry-unmask as={title ? undefined : "div"}>
207
207
  {title}
208
208
  </Title>
209
209
  {clearFormButton && !clearOnBottom && (
@@ -1,4 +1,4 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { render, screen, fireEvent } from "@testing-library/react";
2
2
  import React from "react";
3
3
  import { withTheme } from "../../hocs/withTheme";
4
4
  // import userEvent from "@testing-library/user-event";
@@ -104,4 +104,49 @@ describe("ImagePicker", () => {
104
104
  const id = screen.getByRole("img");
105
105
  expect(id).toBeInTheDocument();
106
106
  });
107
+
108
+ describe("FakeTextLink keyboard accessibility", () => {
109
+ const alternateActionText = "upload photo";
110
+ const onPickerClick = jest.fn();
111
+
112
+ test("FakeTextLink activates file input on Enter key", () => {
113
+ const { container } = render(
114
+ <ThemedImagePicker onPickerClick={onPickerClick} alternateActionText={alternateActionText} />,
115
+ );
116
+ const fakeLink = screen.getByRole("button", { name: alternateActionText });
117
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
118
+ const clickSpy = jest.spyOn(fileInput, "click");
119
+ fireEvent.keyDown(fakeLink, { key: "Enter" });
120
+ expect(clickSpy).toHaveBeenCalledTimes(1);
121
+ });
122
+
123
+ test("FakeTextLink activates file input on Space key", () => {
124
+ const { container } = render(
125
+ <ThemedImagePicker onPickerClick={onPickerClick} alternateActionText={alternateActionText} />,
126
+ );
127
+ const fakeLink = screen.getByRole("button", { name: alternateActionText });
128
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
129
+ const clickSpy = jest.spyOn(fileInput, "click");
130
+ fireEvent.keyDown(fakeLink, { key: " " });
131
+ expect(clickSpy).toHaveBeenCalledTimes(1);
132
+ });
133
+
134
+ test("FakeTextLink is not keyboard-focusable when disabled", () => {
135
+ render(<ThemedImagePicker onPickerClick={onPickerClick} alternateActionText={alternateActionText} disabled />);
136
+ const fakeLink = screen.getByRole("button", { name: alternateActionText });
137
+ expect(fakeLink).toHaveAttribute("tabindex", "-1");
138
+ expect(fakeLink).toHaveAttribute("aria-disabled", "true");
139
+ });
140
+
141
+ test("FakeTextLink does not activate file input when disabled", () => {
142
+ const { container } = render(
143
+ <ThemedImagePicker onPickerClick={onPickerClick} alternateActionText={alternateActionText} disabled />,
144
+ );
145
+ const fakeLink = screen.getByRole("button", { name: alternateActionText });
146
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
147
+ const clickSpy = jest.spyOn(fileInput, "click");
148
+ fireEvent.keyDown(fakeLink, { key: "Enter" });
149
+ expect(clickSpy).not.toHaveBeenCalled();
150
+ });
151
+ });
107
152
  });
@@ -192,12 +192,24 @@ export const ImagePicker: FC<ImagePickerType> = ({
192
192
  }
193
193
  }, [inputGroupMeasure.width, showValidationMessage, imageLabel, validationMessage]);
194
194
 
195
+ const textInputRef = useRef<HTMLInputElement>(null);
196
+ const handleFakeTextLinkKeyDown = (e: React.KeyboardEvent) => {
197
+ if (disabled || e.repeat) return;
198
+ if (e.key === "Enter" || e.key === " ") {
199
+ e.preventDefault();
200
+ textInputRef.current?.click();
201
+ }
202
+ };
203
+
195
204
  // Clear input
196
205
  const inputRef = useRef<HTMLInputElement>(null);
197
206
  const clearInput = () => {
198
207
  if (inputRef.current) {
199
208
  inputRef.current.value = "";
200
209
  }
210
+ if (textInputRef.current) {
211
+ textInputRef.current.value = "";
212
+ }
201
213
  setTempFile("");
202
214
  setPlaceholder("");
203
215
 
@@ -299,6 +311,7 @@ export const ImagePicker: FC<ImagePickerType> = ({
299
311
  disabled={disabled}
300
312
  name={name}
301
313
  onChange={handleInputOnChange}
314
+ ref={inputRef}
302
315
  required={required}
303
316
  type="file"
304
317
  $showValidationMessage={showValidationMessage}
@@ -350,16 +363,26 @@ export const ImagePicker: FC<ImagePickerType> = ({
350
363
  disabled={disabled}
351
364
  onChange={handleInputOnChange}
352
365
  name={name}
366
+ ref={textInputRef}
353
367
  required={required}
354
368
  $showValidationMessage={showValidationMessage}
355
369
  $themeOverride={themeOverride}
370
+ tabIndex={-1}
356
371
  height={20}
357
372
  width={20}
358
373
  type="file"
359
374
  capture={capture}
360
375
  />
361
376
  <BodySPrimary $lineHeight={24} as="span">
362
- <FakeTextLink $themeOverride={themeOverride}>{alternateActionText}</FakeTextLink>
377
+ <FakeTextLink
378
+ $themeOverride={themeOverride}
379
+ tabIndex={disabled ? -1 : 0}
380
+ role="button"
381
+ aria-disabled={disabled}
382
+ onKeyDown={handleFakeTextLinkKeyDown}
383
+ >
384
+ {alternateActionText}
385
+ </FakeTextLink>
363
386
  </BodySPrimary>
364
387
  </TextInputContainer>
365
388
  </AlternatePickerText>
@@ -400,16 +423,26 @@ export const ImagePicker: FC<ImagePickerType> = ({
400
423
  disabled={disabled}
401
424
  onChange={handleInputOnChange}
402
425
  name={name}
426
+ ref={textInputRef}
403
427
  required={required}
404
428
  $showValidationMessage={showValidationMessage}
405
429
  $themeOverride={themeOverride}
430
+ tabIndex={-1}
406
431
  height={20}
407
432
  width={20}
408
433
  type="file"
409
434
  capture={capture}
410
435
  />
411
436
  <BodySPrimary $lineHeight={24} as="span">
412
- <FakeTextLink $themeOverride={themeOverride}>{alternateActionText}</FakeTextLink>
437
+ <FakeTextLink
438
+ $themeOverride={themeOverride}
439
+ tabIndex={disabled ? -1 : 0}
440
+ role="button"
441
+ aria-disabled={disabled}
442
+ onKeyDown={handleFakeTextLinkKeyDown}
443
+ >
444
+ {alternateActionText}
445
+ </FakeTextLink>
413
446
  </BodySPrimary>
414
447
  </TextInputContainer>
415
448
  </AlternatePickerText>
@@ -205,6 +205,12 @@ export const FakeTextLink = styled.span<FakeTextLinkProps>`
205
205
  color: ${({ $themeOverride, theme }) => $themeOverride?.actionTextColor || theme.imagePicker.actionTextColor};
206
206
  padding-bottom: 4px;
207
207
  position: relative;
208
+
209
+ &:focus-visible {
210
+ outline: 1px solid ${({ $themeOverride, theme }) => $themeOverride?.focusColor || theme.focusColor};
211
+ outline-offset: 2px;
212
+ border-radius: 2px;
213
+ }
208
214
  `;
209
215
 
210
216
  export const AlternatePickerText = styled(BodyXSPrimary)<AlternatePickerTextProps>`
@@ -23,6 +23,7 @@ export const RJSForm: FC<RJSFormType> = (props) => {
23
23
  uiSchema,
24
24
  loading,
25
25
  backButton,
26
+ buttonShadow,
26
27
  nextButton,
27
28
  formData: initialValues,
28
29
  } = props;
@@ -75,6 +76,7 @@ export const RJSForm: FC<RJSFormType> = (props) => {
75
76
  text={submitButtonText}
76
77
  nextButton={nextButton}
77
78
  backButton={backButton}
79
+ shadow={buttonShadow ?? true}
78
80
  loading={loading}
79
81
  />
80
82
  )}
@@ -28,6 +28,8 @@ export interface RJSFormType extends Omit<FormProps, "schema" | "validator"> {
28
28
  loading?: boolean;
29
29
  /** Adds an arrow pointing left and pushes the text right */
30
30
  backButton?: boolean;
31
+ /** Adds a shadow to the button. Defaults to true */
32
+ buttonShadow?: boolean;
31
33
  /** Adds an arrow pointing right and pushes the text left */
32
34
  nextButton?: boolean;
33
35
  // TODO: install this
@@ -10,7 +10,7 @@ export const RadioContainer = styled.span<RadioContainerProps>`
10
10
  z-index: 0;
11
11
  &:after {
12
12
  content: "";
13
- border: 7px solid ${({ $focusedColor, theme }) => $focusedColor || theme.switch?.focusedBorderColor};
13
+ border: 1px solid ${({ $focusedColor, theme }) => $focusedColor || theme.switch?.focusedBorderColor};
14
14
  border-radius: 50%;
15
15
  top: -5px;
16
16
  bottom: -5px;
@@ -8,7 +8,7 @@ export const RadioItemWrapper = styled(RadioContainer)<RadioItemProps>`
8
8
  pointer-events: ${({ $disabled }) => ($disabled ? "none" : "all")};
9
9
  -webkit-tap-highlight-color: transparent;
10
10
  ${RadioContainer}:after {
11
- border: 7px solid ${({ $focusedColor, theme }) => $focusedColor || theme.switch?.focusedBorderColor};
11
+ border: 1px solid ${({ $focusedColor, theme }) => $focusedColor || theme.switch?.focusedBorderColor};
12
12
  }
13
13
  &:focus-within {
14
14
  outline: none;
@@ -43,14 +43,14 @@ export const KnobColor = styled.div<KnobColorProps>`
43
43
  background-color: ${({ $disabled, $disabledKnobColor, $knobColor, theme }) =>
44
44
  $disabled ? $disabledKnobColor || theme.toggle?.disabledKnobColor : $knobColor || theme.toggle?.knobColor};
45
45
  border-radius: 50%;
46
- box-shadow: ${({ $disabled, theme }) => ($disabled ? `0 2px 2px 0 ${colors.COOL_GREY_20}` : theme.toggle?.knobShadow || "0 2px 2px 0 rgba(0, 0, 0, 0.2)")};
46
+ box-shadow: ${({ $disabled, theme }) => ($disabled ? `0 2px 2px 0 ${colors.COOL_GREY_20}` : theme.toggle?.knobShadow || "0 2px 9px rgba(81,118,236,0.44)")};
47
47
  height: 100%;
48
48
  position: relative;
49
49
  z-index: 1;
50
50
  `;
51
51
  export const Focus = styled.div<FocusProps>`
52
52
  position: absolute;
53
- background-color: ${({ $focusedColor, theme }) => $focusedColor || theme.focusColor};
53
+ border: 1px solid ${({ $focusedColor, theme }) => $focusedColor || theme.focusColor};
54
54
  border-radius: 50%;
55
55
  bottom: -7px;
56
56
  left: -7px;
@@ -58,7 +58,7 @@ export const Focus = styled.div<FocusProps>`
58
58
  right: -7px;
59
59
  top: -7px;
60
60
  ${ClickPadding}:focus-visible & {
61
- opacity: 0.2;
61
+ opacity: 1;
62
62
  }
63
63
  `;
64
64
 
@@ -14,7 +14,7 @@ export const Label = styled.label<LabelProps>`
14
14
  vertical-align: middle;
15
15
  &:focus-within {
16
16
  ${Focus} {
17
- opacity: 0.2;
17
+ opacity: 1;
18
18
  }
19
19
  }
20
20
  `;
package/src/theme.ts CHANGED
@@ -964,7 +964,7 @@ export const cerebellumTheme: DefaultTheme = {
964
964
  },
965
965
  iconButton: {
966
966
  backgroundColor: "transparent",
967
- border: "none", // for highContrast support
967
+ border: "none", // for highContrast support
968
968
  borderRadiusString: "50%",
969
969
  },
970
970
  iconTile: {
@@ -980,7 +980,7 @@ export const cerebellumTheme: DefaultTheme = {
980
980
  },
981
981
  imagePicker: {
982
982
  actionButtonGap: 6,
983
- actionTextColor: colors.BLUE_100,
983
+ actionTextColor: colors.BLUE_100,
984
984
  asteriskColor: colors.BLUE_70,
985
985
  borderColor: colors.COOL_GREY_25,
986
986
  borderRadius: 5,
@@ -1210,7 +1210,7 @@ export const cerebellumTheme: DefaultTheme = {
1210
1210
  checkColor: "transparent",
1211
1211
  disabledColor: colors.COOL_GREY_25,
1212
1212
  disabledColorLight: colors.COOL_GREY_15,
1213
- focusedBorderColor: colors.BLUE_5,
1213
+ focusedBorderColor: colors.BLUE_70,
1214
1214
  inactiveColor: colors.COOL_GREY_60,
1215
1215
  },
1216
1216
  switchItem: {
@@ -1271,7 +1271,7 @@ export const cerebellumTheme: DefaultTheme = {
1271
1271
  disabledTrackColor: colors.AQUA_20,
1272
1272
  disabledTrackBorderColor: colors.COOL_GREY_25,
1273
1273
  knobColor: colors.WHITE,
1274
- knobShadow: "0 2px 2px 0 rgba(0, 0, 0, 0.2)",
1274
+ knobShadow: "0 2px 9px rgba(81,118,236,0.44)",
1275
1275
  inactiveTrackBorderColor: colors.COOL_GREY_25,
1276
1276
  inactiveTrackBorderColorHover: colors.COOL_GREY_40,
1277
1277
  inactiveTrackColor: colors.COOL_GREY_25,
@@ -1701,7 +1701,7 @@ export const highContrastTheme: DefaultTheme = {
1701
1701
  switch: {
1702
1702
  activeColor: colors.BLACK,
1703
1703
  checkColor: "transparent",
1704
- focusedBorderColor: colors.BLUE_5,
1704
+ focusedBorderColor: colors.BLUE_70,
1705
1705
  inactiveColor: colors.BLACK,
1706
1706
  disabledColor: colors.COOL_GREY_25,
1707
1707
  disabledColorLight: colors.COOL_GREY_15,