@cerebruminc/cerebellum 17.3.1 → 17.3.2-beta.dangerous.9e2daf3

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.3.2](https://github.com/cerebruminc/cerebellum/compare/v17.3.1...v17.3.2) (2026-06-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **toggle:** add aria-checked and aria-label to Toggle switch (FRN-3162) ([06a1896](https://github.com/cerebruminc/cerebellum/commit/06a1896e1712b5accfe0d95baccdb7b23af7c75a))
9
+
3
10
  ## [17.3.1](https://github.com/cerebruminc/cerebellum/compare/v17.3.0...v17.3.1) (2026-06-11)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerebruminc/cerebellum",
3
- "version": "17.3.1",
3
+ "version": "17.3.2-beta.dangerous.9e2daf3",
4
4
  "description": "Cerebrum's React Component Library",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -89,7 +89,13 @@ export const Button: FC<ButtonType> = (props: ButtonType) => {
89
89
  const arrowGap = directionalButtonGap || theme.button?.directionalButtonGap;
90
90
  const confirmedCaretGap = caretGap || theme.button?.caretGap;
91
91
  const confirmedIconGap = iconGap ?? (boxed || textButton ? 10 : boxedOutline ? 13 : Icon ? 20 : arrowGap || 70);
92
- const iconColor = disabled ? disabledTextColor || theme.button?.disabledTextColor : primary && !white ? colorGroup.text : colorGroup.main;
92
+ const iconColor = disabled
93
+ ? disabledTextColor || theme.button?.disabledTextColor
94
+ : primary && !white
95
+ ? colorGroup.text
96
+ : secondary || boxed
97
+ ? colorGroup.secondaryText || colorGroup.main
98
+ : colorGroup.main;
93
99
  const loadingStyleFormat =
94
100
  loadingConfiguration || getLoadingConfiguration({ primary, outline, boxedOutline, secondary, textButton, boxed, colorGroup });
95
101
  const loadingOffsetParams = { iconSize, autoIconSize, iconGap, confirmedIconGap, Icon, backButton, caret, nextButton };
@@ -182,7 +188,9 @@ export const Button: FC<ButtonType> = (props: ButtonType) => {
182
188
  $disabled={disabled}
183
189
  $disabledTextColor={disabledTextColor}
184
190
  $letterSpacing={letterSpacing}
191
+ $outline={outline}
185
192
  $primary={!(!primary || white)}
193
+ $secondary={secondary}
186
194
  $textButton={textButton}
187
195
  $textFontSize={textFontSize}
188
196
  data-sentry-unmask
@@ -0,0 +1,60 @@
1
+ import { colors } from "../../const/colors";
2
+ import { contrast } from "../../helpers/accessibility";
3
+ import { cerebellumTheme, cortexTheme, highContrastTheme } from "../../theme";
4
+
5
+ const WCAG_AA = 4.5;
6
+ const WHITE = colors.WHITE;
7
+ const COOL_GREY_5 = colors.COOL_GREY_5;
8
+
9
+ describe("Button color family WCAG AA compliance", () => {
10
+ const themes = {
11
+ cerebellum: cerebellumTheme,
12
+ cortex: cortexTheme,
13
+ highContrast: highContrastTheme,
14
+ };
15
+
16
+ for (const [themeName, theme] of Object.entries(themes)) {
17
+ describe(`${themeName} theme`, () => {
18
+ for (const [familyName, family] of Object.entries(theme.colorFamilies)) {
19
+ describe(`${familyName} color family`, () => {
20
+ const secondaryText = family.secondaryText || family.main;
21
+
22
+ test("PrimaryButton: white text on main background meets AA", () => {
23
+ const ratio = contrast(WHITE, family.main);
24
+ expect(ratio).toBeGreaterThanOrEqual(WCAG_AA);
25
+ });
26
+
27
+ test("PrimaryButton hover: white text on hover background meets AA", () => {
28
+ const ratio = contrast(WHITE, family.hover);
29
+ expect(ratio).toBeGreaterThanOrEqual(WCAG_AA);
30
+ });
31
+
32
+ test("OutlineButton/TextButton: main text on white meets AA", () => {
33
+ const ratio = contrast(family.main, WHITE);
34
+ expect(ratio).toBeGreaterThanOrEqual(WCAG_AA);
35
+ });
36
+
37
+ test("OutlineButton hover: hover text on COOL_GREY_5 meets AA", () => {
38
+ const ratio = contrast(family.hover, COOL_GREY_5);
39
+ expect(ratio).toBeGreaterThanOrEqual(WCAG_AA);
40
+ });
41
+
42
+ test("SecondaryButton: secondaryText on light background meets AA", () => {
43
+ const ratio = contrast(secondaryText, family.light);
44
+ expect(ratio).toBeGreaterThanOrEqual(WCAG_AA);
45
+ });
46
+
47
+ test("SecondaryButton hover: secondaryText on medium background meets AA", () => {
48
+ const ratio = contrast(secondaryText, family.medium);
49
+ expect(ratio).toBeGreaterThanOrEqual(WCAG_AA);
50
+ });
51
+
52
+ test("TextButton hover: secondaryText on light background meets AA", () => {
53
+ const ratio = contrast(secondaryText, family.light);
54
+ expect(ratio).toBeGreaterThanOrEqual(WCAG_AA);
55
+ });
56
+ });
57
+ }
58
+ });
59
+ }
60
+ });
@@ -88,6 +88,8 @@ export const ButtonElement = styled.button<ButtonElementProps>`
88
88
  ? $colorGroup.medium
89
89
  : $colorGroup.hover;
90
90
  }};
91
+ ${({ $boxedOutline, $outline, $colorGroup, disabled }) =>
92
+ ($boxedOutline || $outline) && !disabled ? `border-color: ${$colorGroup.hover};` : ""}
91
93
  }
92
94
  &:after {
93
95
  content: "";
@@ -142,15 +144,36 @@ export const NextIconBox = styled(IconBox)`
142
144
  margin-right: 0;
143
145
  `;
144
146
  export const Text = styled.span<TextProps>`
145
- color: ${({ $primary, $colorGroup, $disabled, $disabledTextColor, theme }) =>
146
- $disabled ? $disabledTextColor || theme.button?.disabledTextColor : $primary ? $colorGroup.text : $colorGroup.main};
147
+ color: ${({ $primary, $secondary, $boxed, $colorGroup, $disabled, $disabledTextColor, theme }) =>
148
+ $disabled
149
+ ? $disabledTextColor || theme.button?.disabledTextColor
150
+ : $primary
151
+ ? $colorGroup.text
152
+ : $secondary || $boxed
153
+ ? $colorGroup.secondaryText || $colorGroup.main
154
+ : $colorGroup.main};
147
155
  font-size: ${(props) => props.$textFontSize || props.theme.button?.textFontSize}px;
148
156
  font-weight: ${({ $boxed, $boxedOutline, $textButton }) => ($boxed || $boxedOutline || $textButton ? 500 : 600)};
149
157
  letter-spacing: ${({ $boxed, $boxedOutline, $letterSpacing, $textButton: textButton }) => ($letterSpacing ? $letterSpacing : ($boxed || $boxedOutline || textButton ? "0.36px" : "0.07em"))};
150
158
  position: relative;
159
+ transition: color 150ms ease-out;
151
160
  vertical-align: middle;
152
161
  white-space: nowrap;
153
162
  z-index: 1;
163
+ ${ButtonElement}:hover & {
164
+ color: ${({ $primary, $secondary, $boxed, $outline, $boxedOutline, $textButton, $colorGroup, $disabled, $disabledTextColor, theme }) =>
165
+ $disabled
166
+ ? $disabledTextColor || theme.button?.disabledTextColor
167
+ : $primary
168
+ ? $colorGroup.text
169
+ : $outline || $boxedOutline
170
+ ? $colorGroup.hover
171
+ : $textButton
172
+ ? $colorGroup.secondaryText || $colorGroup.main
173
+ : $secondary || $boxed
174
+ ? $colorGroup.secondaryText || $colorGroup.main
175
+ : $colorGroup.main};
176
+ }
154
177
  `;
155
178
  export const TextButtonBackground = styled.span<TextBackgroundProps>`
156
179
  bottom: 0;
@@ -69,7 +69,7 @@ export const getLoadingConfiguration = ({
69
69
  if (primary) {
70
70
  return PrimaryButtonLoadingConfiguration(colorGroup?.text);
71
71
  } else if (secondary) {
72
- return SecondaryButtonLoadingConfiguration(colorGroup?.main);
72
+ return SecondaryButtonLoadingConfiguration(colorGroup?.secondaryText || colorGroup?.main);
73
73
  } else if (outline) {
74
74
  return OutlineButtonLoadingConfiguration(colorGroup?.main);
75
75
  } else if (boxedOutline) {
@@ -77,7 +77,7 @@ export const getLoadingConfiguration = ({
77
77
  } else if (textButton) {
78
78
  return TextButtonLoadingConfiguration(colorGroup?.main);
79
79
  } else if (boxed) {
80
- return BoxedButtonLoadingConfiguration(colorGroup?.main);
80
+ return BoxedButtonLoadingConfiguration(colorGroup?.secondaryText || colorGroup?.main);
81
81
  } else {
82
82
  return {};
83
83
  }
@@ -164,7 +164,9 @@ export interface TextProps {
164
164
  $disabled?: boolean;
165
165
  $disabledTextColor?: string;
166
166
  $letterSpacing?: string;
167
+ $outline?: boolean;
167
168
  $primary: boolean;
169
+ $secondary?: boolean;
168
170
  $textButton?: boolean;
169
171
  $textFontSize?: number;
170
172
  }
@@ -17,6 +17,7 @@ type Story = StoryObj<typeof Toggle>;
17
17
  export const Default: Story = {
18
18
  args: {
19
19
  active: true,
20
+ "aria-label": "Toggle switch",
20
21
  disabled: false,
21
22
  onToggle: () => console.log("ToggleClick"),
22
23
  },
@@ -20,6 +20,39 @@ describe("Toggle", () => {
20
20
  });
21
21
  });
22
22
 
23
+ test("has aria-checked true when active", () => {
24
+ render(<ThemedToggle active onToggle={jest.fn()} />);
25
+ const toggle = screen.getByRole("switch");
26
+ expect(toggle).toHaveAttribute("aria-checked", "true");
27
+ });
28
+
29
+ test("has aria-checked false when inactive", () => {
30
+ render(<ThemedToggle active={false} onToggle={jest.fn()} />);
31
+ const toggle = screen.getByRole("switch");
32
+ expect(toggle).toHaveAttribute("aria-checked", "false");
33
+ });
34
+
35
+ test("sets aria-label when provided", () => {
36
+ render(<ThemedToggle active onToggle={jest.fn()} aria-label="Dark mode" />);
37
+ const toggle = screen.getByRole("switch", { name: "Dark mode" });
38
+ expect(toggle).toBeInTheDocument();
39
+ });
40
+
41
+ test("toggles on Space key press", async () => {
42
+ const mockOnToggle = jest.fn();
43
+ render(<ThemedToggle active onToggle={mockOnToggle} />);
44
+ const toggle = screen.getByRole("switch");
45
+ toggle.focus();
46
+ await userEvent.keyboard(" ");
47
+ expect(mockOnToggle).toHaveBeenCalledTimes(1);
48
+ });
49
+
50
+ test("has aria-disabled true when disabled", () => {
51
+ render(<ThemedToggle active={false} disabled onToggle={jest.fn()} />);
52
+ const toggle = screen.getByRole("switch");
53
+ expect(toggle).toHaveAttribute("aria-disabled", "true");
54
+ });
55
+
23
56
  test("renders disabled Toggle", async () => {
24
57
  const mockOnClick = jest.fn();
25
58
  render(<ThemedToggle active={false} disabled onToggle={mockOnClick} />);
@@ -1,12 +1,13 @@
1
1
  import React, { FC, KeyboardEvent } from "react";
2
2
 
3
- import { pressedEnter } from "../../helpers/accessibility";
3
+ import { pressedEnter, pressedSpacebar } from "../../helpers/accessibility";
4
4
 
5
5
  import { ActiveTrack, Base, ClickPadding, Focus, Knob, KnobColor, TrackBox } from "./ToggleStyles";
6
6
  import { ToggleType } from "./types";
7
7
 
8
8
  export const Toggle: FC<ToggleType> = ({
9
9
  active,
10
+ "aria-label": ariaLabel,
10
11
  clickPadding = 15,
11
12
  children,
12
13
  disabled,
@@ -33,8 +34,17 @@ export const Toggle: FC<ToggleType> = ({
33
34
  onToggle?.(event);
34
35
  }
35
36
  }}
37
+ onKeyDown={(event: KeyboardEvent<HTMLSpanElement>) => {
38
+ if (!disabled && pressedSpacebar(event)) {
39
+ event.preventDefault();
40
+ onToggle?.(event);
41
+ }
42
+ }}
36
43
  $disabled={disabled}
37
44
  role="switch"
45
+ aria-checked={active}
46
+ aria-disabled={disabled}
47
+ aria-label={ariaLabel}
38
48
  tabIndex={!onToggle || disabled ? -1 : 0}
39
49
  >
40
50
  <Base disabled={disabled} $knobSize={knobSize} $trackHeight={trackHeight} $width={width}>
@@ -3,6 +3,8 @@ import { KeyboardEvent, MouseEvent } from "react";
3
3
  export interface ToggleType {
4
4
  /** The current state of the Toggle */
5
5
  active: boolean;
6
+ /** Accessible label for the toggle switch. Required when Toggle is used without a visible label or wrapping <label> element. */
7
+ "aria-label"?: string;
6
8
  children?: React.ReactNode;
7
9
  /** Clickable space added around Toggle, in pixels. Pass `0` to disable */
8
10
  clickPadding?: number | string;
@@ -138,8 +138,8 @@ describe("ToggleButtons", () => {
138
138
 
139
139
  const iconBox = screen.getByTestId(`${buttonText} icon`);
140
140
  const path = iconBox.querySelector("path");
141
- // Default: active icon fill = colorGroup.hover = BLUE_100
142
- expect(path?.getAttribute("fill")).toBe("#4F6CEA");
141
+ // Default: active icon fill = colorGroup.hover = BLUE_120
142
+ expect(path?.getAttribute("fill")).toBe("#4965c8");
143
143
  });
144
144
 
145
145
  test("active icon uses colorGroup.light fill when invertActive is true", () => {
@@ -82,13 +82,13 @@ const blue: MantineColorsTuple = [
82
82
  "#E5EBFC", // BLUE_5 - 0
83
83
  "#D7E1FF", // BLUE_10 - 1
84
84
  "#ADBDFE", // BLUE_25 - 2
85
- "#829EFD", // BLUE_40 - 3
86
- "#7290F7", // BLUE_50 - 4
87
- "#6b8af5", // BLUE_65 - 5
88
- "#5d7cf0", // BLUE_80 - 6
89
- "#4F6CEA", // BLUE_100 - 7
90
- "#4d6be1", // BLUE_110 - 8
91
- "#4965c8", // BLUE_120 - 9
85
+ "#7A97FA", // BLUE_45 - 3
86
+ "#6988f4", // BLUE_70 - 4
87
+ "#5d7cf0", // BLUE_80 - 5
88
+ "#4F6CEA", // BLUE_100 - 6 (main)
89
+ "#4965c8", // BLUE_120 - 7 (hover)
90
+ "#4158aa", // BLUE_130 - 8 (secondaryText)
91
+ "#394B91", // BLUE_150 - 9
92
92
  ];
93
93
 
94
94
  // High-contrast binary palette — mirrors styled-components highContrastColorFamily (BLACK/WHITE only)
@@ -288,57 +288,57 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
288
288
  ? highContrastRed
289
289
  : [
290
290
  "#FFDDE5", // RED_5 - 0
291
- "#FFCCD4", // RED_15 - 1
291
+ "#FFD5DD", // RED_10 - 1
292
292
  "#FFBBC4", // RED_25 - 2
293
- "#FFAAB4", // RED_35 - 3
294
- "#FF8F9C", // RED_50 - 4
295
- "#FF7285", // RED_65 - 5
296
- "#FF506F", // RED_80 - 6
297
- "#FF3161", // RED_100 - 7
298
- "#EB305A", // RED_110 - 8
299
- "#D72F53", // RED_120 - 9
293
+ "#FF8F9C", // RED_50 - 3
294
+ "#FF677E", // RED_70 - 4
295
+ "#FF3161", // RED_100 - 5
296
+ "#D72F53", // RED_120 - 6 (main)
297
+ "#C42D4C", // RED_130 - 7 (hover)
298
+ "#B02B45", // RED_140 - 8 (secondaryText)
299
+ "#9E293E", // RED_150 - 9
300
300
  ],
301
301
  peach: highContrast
302
302
  ? highContrastGray
303
303
  : [
304
304
  "#FFF1EC", // PEACH_5 - 0
305
- "#FFE5DE", // PEACH_15 - 1
305
+ "#FFEBE5", // PEACH_10 - 1
306
306
  "#FFD9D1", // PEACH_25 - 2
307
- "#FFCDC3", // PEACH_35 - 3
308
- "#FFBBAE", // PEACH_50 - 4
309
- "#FFAA9A", // PEACH_65 - 5
310
- "#FF9885", // PEACH_80 - 6
311
- "#FF806A", // PEACH_100 - 7
312
- "#ED7762", // PEACH_110 - 8
313
- "#DB6E5B", // PEACH_120 - 9
307
+ "#FFBBAE", // PEACH_50 - 3
308
+ "#FFA493", // PEACH_70 - 4
309
+ "#FF806A", // PEACH_100 - 5
310
+ "#B65B4C", // PEACH_140 - 6 (main)
311
+ "#A45244", // PEACH_150 - 7 (hover)
312
+ "#92493D", // PEACH_160 - 8 (secondaryText)
313
+ "#804035", // PEACH_170 - 9
314
314
  ],
315
315
  purple: highContrast
316
316
  ? highContrastGray
317
317
  : [
318
318
  "#F0EAFF", // PURPLE_5 - 0
319
- "#dbcdff", // PURPLE_15 - 1
319
+ "#e5dbff", // PURPLE_10 - 1
320
320
  "#c6afff", // PURPLE_25 - 2
321
- "#b192ff", // PURPLE_35 - 3
321
+ "#9c75ff", // PURPLE_45 - 3
322
322
  "#8a5aff", // PURPLE_55 - 4
323
323
  "#8346ff", // PURPLE_65 - 5
324
- "#7928ff", // PURPLE_80 - 6
325
- "#6B00FF", // PURPLE_100 - 7
326
- "#6300ED", // PURPLE_110 - 8
324
+ "#803cff", // PURPLE_70 - 6 (main)
325
+ "#7928ff", // PURPLE_80 - 7 (hover)
326
+ "#6B00FF", // PURPLE_100 - 8 (secondaryText)
327
327
  "#5C00DB", // PURPLE_120 - 9
328
328
  ],
329
329
  aqua: highContrast
330
330
  ? highContrastGray
331
331
  : [
332
332
  "#e0f7f2", // AQUA_5 - 0
333
- "#cdf2ea", // AQUA_15 - 1
333
+ "#d7f5ee", // AQUA_10 - 1
334
334
  "#baede2", // AQUA_25 - 2
335
- "#a7e8da", // AQUA_35 - 3
336
- "#8be1ce", // AQUA_50 - 4
337
- "#6fdac2", // AQUA_65 - 5
338
- "#53d3b6", // AQUA_80 - 6
339
- "#2dc9a6", // AQUA_100 - 7
340
- "#2ABB9A", // AQUA_110 - 8
341
- "#27AC8E", // AQUA_120 - 9
335
+ "#8be1ce", // AQUA_50 - 3
336
+ "#53d3b6", // AQUA_80 - 4
337
+ "#2dc9a6", // AQUA_100 - 5
338
+ "#1D816B", // AQUA_150 - 6 (main)
339
+ "#1A735F", // AQUA_160 - 7 (hover)
340
+ "#176553", // AQUA_170 - 8 (secondaryText)
341
+ "#135647", // AQUA_180 - 9
342
342
  ],
343
343
  orange: highContrast
344
344
  ? highContrastGray
@@ -18,6 +18,7 @@ export interface ColorFamilyType {
18
18
  hover: string;
19
19
  text: string;
20
20
  outline?: string;
21
+ secondaryText?: string;
21
22
  }
22
23
  export interface ConnotationColorType {
23
24
  light: string;
package/src/theme.ts CHANGED
@@ -757,9 +757,10 @@ export const cerebellumTheme: DefaultTheme = {
757
757
  medium: colors.AQUA_10,
758
758
  mediumDark: colors.AQUA_25,
759
759
  dark: colors.AQUA_80,
760
- main: colors.AQUA_90,
761
- hover: colors.AQUA_100,
760
+ main: colors.AQUA_150,
761
+ hover: colors.AQUA_160,
762
762
  text: colors.WHITE,
763
+ secondaryText: colors.AQUA_170,
763
764
  },
764
765
  BLACK: {
765
766
  light: colors.COOL_GREY_10,
@@ -775,9 +776,10 @@ export const cerebellumTheme: DefaultTheme = {
775
776
  medium: colors.BLUE_10,
776
777
  mediumDark: colors.BLUE_25,
777
778
  dark: colors.BLUE_45,
778
- main: colors.BLUE_70,
779
- hover: colors.BLUE_100,
779
+ main: colors.BLUE_100,
780
+ hover: colors.BLUE_120,
780
781
  text: colors.WHITE,
782
+ secondaryText: colors.BLUE_130,
781
783
  },
782
784
  COOL_GREY: {
783
785
  light: colors.COOL_GREY_4,
@@ -793,27 +795,30 @@ export const cerebellumTheme: DefaultTheme = {
793
795
  medium: colors.PEACH_10,
794
796
  mediumDark: colors.PEACH_25,
795
797
  dark: colors.PEACH_80,
796
- main: colors.PEACH_90,
797
- hover: colors.PEACH_100,
798
+ main: colors.PEACH_140,
799
+ hover: colors.PEACH_150,
798
800
  text: colors.WHITE,
801
+ secondaryText: colors.PEACH_160,
799
802
  },
800
803
  PURPLE: {
801
804
  light: colors.PURPLE_5,
802
805
  medium: colors.PURPLE_10,
803
806
  mediumDark: colors.PURPLE_25,
804
807
  dark: colors.PURPLE_45,
805
- main: colors.PURPLE_55,
806
- hover: colors.PURPLE_70,
808
+ main: colors.PURPLE_70,
809
+ hover: colors.PURPLE_80,
807
810
  text: colors.WHITE,
811
+ secondaryText: colors.PURPLE_100,
808
812
  },
809
813
  RED: {
810
814
  light: colors.RED_5,
811
815
  medium: colors.RED_10,
812
816
  mediumDark: colors.RED_25,
813
817
  dark: colors.RED_65,
814
- main: colors.RED_100,
815
- hover: colors.RED_100,
818
+ main: colors.RED_120,
819
+ hover: colors.RED_130,
816
820
  text: colors.WHITE,
821
+ secondaryText: colors.RED_140,
817
822
  },
818
823
  },
819
824
  colorPicker: {
@@ -1361,10 +1366,12 @@ const cortexColorFamilies = JSON.parse(JSON.stringify(cerebellumTheme.colorFamil
1361
1366
  cortexColorFamilies.PURPLE = {
1362
1367
  light: colors.PLUM_10,
1363
1368
  medium: colors.PLUM_20,
1369
+ mediumDark: colors.PLUM_40,
1364
1370
  dark: colors.PLUM_40,
1365
1371
  main: colors.PLUM_50,
1366
1372
  hover: colors.PLUM_60,
1367
1373
  text: colors.WHITE,
1374
+ secondaryText: colors.PLUM_60,
1368
1375
  };
1369
1376
 
1370
1377
  export const cortexTheme: DefaultTheme = {