@cerebruminc/cerebellum 17.3.4 → 17.3.5

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,15 @@
1
1
  # react-component-lib-boilerplate
2
2
 
3
+ ## [17.3.5](https://github.com/cerebruminc/cerebellum/compare/v17.3.4...v17.3.5) (2026-06-19)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **deps:** override postcss to ^8.5.10 to resolve CVE-2026-41305 ([8603ab7](https://github.com/cerebruminc/cerebellum/commit/8603ab74f2be7e3fddcaf6a758180b5607710af9))
9
+ * **DigitalInput:** add accessible labels to digit inputs (WCAG 1.3.1, 4.1.2) ([080b765](https://github.com/cerebruminc/cerebellum/commit/080b765c6d2da72b6af3ccee81db622a10f19a1a))
10
+ * **toggle-buttons:** add tabIndex support ([18ee8f9](https://github.com/cerebruminc/cerebellum/commit/18ee8f942e5fcbfb251947925da4cf37e8d82e4c))
11
+ * **toggle-buttons:** use BLUE_130 for active text to meet WCAG AA contrast ([8d1f9ef](https://github.com/cerebruminc/cerebellum/commit/8d1f9ef6ee129e4427389a40960fb00725ab07ca))
12
+
3
13
  ## [17.3.4](https://github.com/cerebruminc/cerebellum/compare/v17.3.3...v17.3.4) (2026-06-17)
4
14
 
5
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerebruminc/cerebellum",
3
- "version": "17.3.4",
3
+ "version": "17.3.5",
4
4
  "description": "Cerebrum's React Component Library",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -153,6 +153,9 @@
153
153
  "publishConfig": {
154
154
  "access": "public"
155
155
  },
156
+ "overrides": {
157
+ "postcss": "^8.5.10"
158
+ },
156
159
  "resolutions": {
157
160
  "**/trim": "^1.0.0"
158
161
  },
@@ -26,7 +26,16 @@ describe("DigitalInput", () => {
26
26
  test("Renders the label", () => {
27
27
  const label = "This is the label";
28
28
  render(<DigitalInput name="test" value="" label={label} />);
29
- expect(screen.getByLabelText(label)).toBeInTheDocument();
29
+ expect(screen.getByText(label)).toBeInTheDocument();
30
+ });
31
+
32
+ test("visible label is programmatically associated with the hidden aggregated input", () => {
33
+ const label = "Verification code";
34
+ const id = "verification-code";
35
+ render(<DigitalInput name="test" value="" label={label} id={id} />);
36
+ const labelEl = screen.getByText(label);
37
+ expect(labelEl).toHaveAttribute("for", id);
38
+ expect(document.getElementById(id)).toBeInTheDocument();
30
39
  });
31
40
 
32
41
  test("Submits value onChange and changes focus", () => {
@@ -188,4 +197,33 @@ describe("DigitalInput", () => {
188
197
  // Expect focus to move to the second input
189
198
  expect(firstInput).not.toHaveFocus();
190
199
  });
200
+
201
+ test("Each digit input has an accessible label", () => {
202
+ const digitLength = 4;
203
+ render(<DigitalInput name="test" value="" digitLength={digitLength} />);
204
+ for (let i = 1; i <= digitLength; i++) {
205
+ expect(screen.getByLabelText(`Digit ${i} of ${digitLength}`)).toBeInTheDocument();
206
+ }
207
+ });
208
+
209
+ test("InputGroup has role=group with aria-label from label prop", () => {
210
+ const label = "Verification code";
211
+ render(<DigitalInput name="test" value="" label={label} />);
212
+ expect(screen.getByRole("group", { name: label })).toBeInTheDocument();
213
+ });
214
+
215
+ test("InputGroup has fallback aria-label when label is not a string", () => {
216
+ render(<DigitalInput name="test" value="" />);
217
+ expect(screen.getByRole("group", { name: "Code input" })).toBeInTheDocument();
218
+ });
219
+
220
+ test("InputGroup has fallback aria-label when label is an empty string", () => {
221
+ render(<DigitalInput name="test" value="" label="" />);
222
+ expect(screen.getByRole("group", { name: "Code input" })).toBeInTheDocument();
223
+ });
224
+
225
+ test("Prefix input has an accessible label", () => {
226
+ render(<DigitalInput name="test" value="" prefix="V" />);
227
+ expect(screen.getByLabelText("Prefix")).toBeInTheDocument();
228
+ });
191
229
  });
@@ -228,11 +228,12 @@ export const DigitalInput: FC<DigitalInputProps> = ({
228
228
  disabledColor: themeOverride?.disabledColor ?? theme.input.disabledColor,
229
229
  labelColor: themeOverride?.labelColor ?? theme.input.labelColor,
230
230
  removePaddingRight: required,
231
+ htmlFor: id,
231
232
  })}
232
233
  {required && <Asterisk $asteriskColor={themeOverride?.asteriskColor ?? theme.input.asteriskColor}>*</Asterisk>}
233
234
  </LabelBox>
234
235
  <HiddenInput
235
- aria-label={typeof label === "string" ? label : "code input"}
236
+ aria-label={typeof label === "string" && label.trim() ? label : "Code input"}
236
237
  disabled={disabled}
237
238
  name={name}
238
239
  id={id}
@@ -242,13 +243,14 @@ export const DigitalInput: FC<DigitalInputProps> = ({
242
243
  type="text"
243
244
  value={value}
244
245
  />
245
- <InputGroup $themeOverride={themeOverride}>
246
+ <InputGroup $themeOverride={themeOverride} role="group" aria-label={typeof label === "string" && label.trim() ? label : "Code input"}>
246
247
  {prefix && (
247
248
  <>
248
249
  <InlineInput
249
250
  key="prefixInput"
250
251
  {...getCommonInputProps()}
251
252
  clickable={false}
253
+ inputLabel="Prefix"
252
254
  onChange={noop}
253
255
  onClick={noop}
254
256
  onInputBlur={noop}
@@ -267,6 +269,7 @@ export const DigitalInput: FC<DigitalInputProps> = ({
267
269
  <InlineInput
268
270
  key={`digitalInput_${index}`}
269
271
  {...getCommonInputProps()}
272
+ inputLabel={`Digit ${index + 1} of ${digitLength}`}
270
273
  autofocus={autofocus && index === 0 ? true : undefined}
271
274
  onChange={(event) => handleInputChange({ index, event: event as unknown as React.ChangeEvent<HTMLInputElement> })}
272
275
  onClick={() => handleInputClick(index)}
@@ -194,7 +194,6 @@ export const InlineInput: FC<InlineInputType> = ({
194
194
  aria-invalid={showValidationMessage ? "true" : undefined}
195
195
  aria-required={required ? "true" : undefined}
196
196
  required={required}
197
- role="textbox"
198
197
  $textAlign={textAlign}
199
198
  $textColor={textColor}
200
199
  $textFontSize={textFontSize}
@@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import React from "react";
4
4
  import { withTheme } from "../../hocs/withTheme";
5
+ import { ColorFamilyType } from "../../sharedTypes/types";
5
6
  import { Settings } from "../Icons";
6
7
  import { ToggleButtons } from "./ToggleButtons";
7
8
  import { ToggleButtonButtonsType } from "./types";
@@ -126,7 +127,7 @@ describe("ToggleButtons", () => {
126
127
  expect(icon).toBeInTheDocument();
127
128
  });
128
129
 
129
- test("active icon uses colorGroup.hover fill by default", () => {
130
+ test("active icon uses colorGroup.secondaryText fill by default", () => {
130
131
  const buttonId = "1";
131
132
  const buttonText = "Foo";
132
133
  const buttons: ToggleButtonButtonsType[] = [
@@ -138,8 +139,41 @@ describe("ToggleButtons", () => {
138
139
 
139
140
  const iconBox = screen.getByTestId(`${buttonText} icon`);
140
141
  const path = iconBox.querySelector("path");
141
- // Default: active icon fill = colorGroup.hover = BLUE_100
142
- expect(path?.getAttribute("fill")).toBe("#4965c8");
142
+ // Default: active icon fill = colorGroup.secondaryText = BLUE_130
143
+ expect(path?.getAttribute("fill")).toBe("#4158aa");
144
+ });
145
+
146
+ test("active icon fill falls back to colorGroup.main when secondaryText is undefined", () => {
147
+ const buttonId = "1";
148
+ const buttonText = "Foo";
149
+ const buttons: ToggleButtonButtonsType[] = [
150
+ { id: buttonId, buttonText: buttonText, Icon: Settings },
151
+ { id: "2", buttonText: "Bar", Icon: Settings },
152
+ ];
153
+
154
+ const colorFamilyWithoutSecondaryText: ColorFamilyType = {
155
+ light: "#ffffff",
156
+ medium: "#cccccc",
157
+ mediumDark: "#999999",
158
+ dark: "#666666",
159
+ main: "#333333",
160
+ hover: "#111111",
161
+ text: "#ffffff",
162
+ // no secondaryText — should fall back to main
163
+ };
164
+
165
+ render(
166
+ <ThemedToggleButtons
167
+ activeId={buttonId}
168
+ buttons={buttons}
169
+ activeColorFamilyOverride={colorFamilyWithoutSecondaryText}
170
+ />,
171
+ );
172
+
173
+ const iconBox = screen.getByTestId(`${buttonText} icon`);
174
+ const path = iconBox.querySelector("path");
175
+ // secondaryText is undefined, so fill should fall back to colorGroup.main
176
+ expect(path?.getAttribute("fill")).toBe("#333333");
143
177
  });
144
178
 
145
179
  test("active icon uses colorGroup.light fill when invertActive is true", () => {
@@ -32,6 +32,7 @@ export const ToggleButtons: FC<ToggleButtonsType> = ({
32
32
  $colorGroup={colorGroup}
33
33
  disabled={isActive}
34
34
  $firstButton={index === 0}
35
+ tabIndex={isActive ? -1 : 0}
35
36
  $fixedButtonWidth={fixedButtonWidth}
36
37
  $invertActive={invertActive}
37
38
  $lastButton={index === buttons.length - 1}
@@ -41,7 +42,9 @@ export const ToggleButtons: FC<ToggleButtonsType> = ({
41
42
  {/* We could use title instead of data-testid, but it would be redundant with buttonText for screen readers */}
42
43
  {Icon && (
43
44
  <IconBox $iconSize={iconSize} data-testid={`${buttonText} icon`} $iconGap={iconGap}>
44
- <Icon fill={isActive ? (invertActive ? colorGroup.light : colorGroup.hover) : inactiveTextColor} />
45
+ <Icon
46
+ fill={isActive ? (invertActive ? colorGroup.light : colorGroup.secondaryText || colorGroup.main) : inactiveTextColor}
47
+ />
45
48
  </IconBox>
46
49
  )}
47
50
  <Text
@@ -10,11 +10,11 @@ export const Button = styled.button<ButtonProps>`
10
10
  align-items: center;
11
11
  background-color: ${({ $active, $colorGroup, $invertActive, $themeOverride, theme }) => {
12
12
  if (!$active) return $themeOverride?.backgroundColor || theme.toggleButtons.backgroundColor;
13
- return $invertActive ? $colorGroup.hover : $colorGroup.light;
13
+ return $invertActive ? $colorGroup.secondaryText || $colorGroup.main : $colorGroup.light;
14
14
  }};
15
15
  border: 1px solid
16
16
  ${({ $active, $colorGroup, $themeOverride, theme }) =>
17
- $active ? $colorGroup.hover : $themeOverride?.borderColor || theme.toggleButtons.borderColor};
17
+ $active ? $colorGroup.secondaryText || $colorGroup.main : $themeOverride?.borderColor || theme.toggleButtons.borderColor};
18
18
  border-radius: ${({ $firstButton, $lastButton, $themeOverride, theme }) => {
19
19
  const radius = `${$themeOverride?.borderRadius || theme.toggleButtons.borderRadius}px`;
20
20
  const topLeft = $firstButton ? radius : 0;
@@ -53,7 +53,7 @@ export const IconBox = styled.div<IconBoxProps>`
53
53
  export const Text = styled.span<TextProps>`
54
54
  color: ${({ $active, $colorGroup, $invertActive, $themeOverride, theme }) => {
55
55
  if (!$active) return $themeOverride?.textColor || theme.toggleButtons.textColor;
56
- return $invertActive ? $colorGroup.light : $colorGroup.hover;
56
+ return $invertActive ? $colorGroup.light : $colorGroup.secondaryText || $colorGroup.main;
57
57
  }};
58
58
  font-size: ${({ $themeOverride, theme }) => $themeOverride?.fontSize || theme.toggleButtons.fontSize}px;
59
59
  font-weight: 600;