@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 +10 -0
- package/package.json +4 -1
- package/src/components/DigitalInput/DigitalInput.test.tsx +39 -1
- package/src/components/DigitalInput/DigitalInput.tsx +5 -2
- package/src/components/InlineInput/InlineInput.tsx +0 -1
- package/src/components/ToggleButtons/ToggleButtons.test.tsx +37 -3
- package/src/components/ToggleButtons/ToggleButtons.tsx +4 -1
- package/src/components/ToggleButtons/ToggleButtonsStyles.tsx +3 -3
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.
|
|
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.
|
|
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 : "
|
|
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.
|
|
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.
|
|
142
|
-
expect(path?.getAttribute("fill")).toBe("#
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|