@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 +23 -0
- package/package.json +1 -1
- package/src/components/Checkbox/CheckboxStyles.tsx +1 -1
- package/src/components/DescriptiveSwitchMenu/DescriptiveSwitchMenuStyles.tsx +2 -2
- package/src/components/Form/Form.tsx +1 -1
- package/src/components/ImagePicker/ImagePicker.test.tsx +46 -1
- package/src/components/ImagePicker/ImagePicker.tsx +35 -2
- package/src/components/ImagePicker/ImagePickerStyles.tsx +6 -0
- package/src/components/RJSForm/RJSForm.tsx +2 -0
- package/src/components/RJSForm/types.ts +2 -0
- package/src/components/Radio/RadioStyles.tsx +1 -1
- package/src/components/RadioItem/RadioItemStyles.tsx +1 -1
- package/src/components/Toggle/ToggleStyles.tsx +3 -3
- package/src/components/ToggleItem/ToggleItemStyles.tsx +1 -1
- package/src/theme.ts +5 -5
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
|
@@ -13,7 +13,7 @@ export const CheckContainer = styled.span<CheckContainerProps>`
|
|
|
13
13
|
z-index: 0;
|
|
14
14
|
&:after {
|
|
15
15
|
content: "";
|
|
16
|
-
border:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
61
|
+
opacity: 1;
|
|
62
62
|
}
|
|
63
63
|
`;
|
|
64
64
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
1704
|
+
focusedBorderColor: colors.BLUE_70,
|
|
1705
1705
|
inactiveColor: colors.BLACK,
|
|
1706
1706
|
disabledColor: colors.COOL_GREY_25,
|
|
1707
1707
|
disabledColorLight: colors.COOL_GREY_15,
|