@cerebruminc/cerebellum 17.3.6 → 17.3.7-beta.dangerous.eb38f7c
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 +7 -0
- package/package.json +1 -1
- package/src/components/DescriptiveSwitchMenu/DescriptiveSwitchMenu.test.tsx +2 -1
- package/src/components/Toggle/Toggle.mdx +4 -3
- package/src/components/Toggle/Toggle.stories.tsx +21 -1
- package/src/components/Toggle/Toggle.test.tsx +17 -0
- package/src/components/Toggle/Toggle.tsx +10 -5
- package/src/components/Toggle/types.ts +3 -1
- package/src/components/ToggleItem/ToggleItem.test.tsx +2 -1
- package/src/components/UploadDropzone/UploadDropzone.test.tsx +40 -1
- package/src/components/UploadDropzone/UploadDropzone.tsx +7 -7
- package/src/theme.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# react-component-lib-boilerplate
|
|
2
2
|
|
|
3
|
+
## [17.3.7](https://github.com/cerebruminc/cerebellum/compare/v17.3.6...v17.3.7) (2026-06-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **upload-dropzone:** increase contrast in UploadDropzone ([ca12fb3](https://github.com/cerebruminc/cerebellum/commit/ca12fb3e092138e49580e00d901dd3dc8a29bd58))
|
|
9
|
+
|
|
3
10
|
## [17.3.6](https://github.com/cerebruminc/cerebellum/compare/v17.3.5...v17.3.6) (2026-06-22)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -60,8 +60,9 @@ describe("DescriptiveSwitchMenu", () => {
|
|
|
60
60
|
|
|
61
61
|
test("Toggle enum displays toggles", () => {
|
|
62
62
|
render(<ThemedDescriptiveSwitchMenu options={options} switchType={SwitchTypeEnum.Toggle} isClickable />);
|
|
63
|
-
const toggles = screen.getAllByRole("
|
|
63
|
+
const toggles = screen.getAllByRole("checkbox");
|
|
64
64
|
expect(toggles.length).toEqual(options.length);
|
|
65
|
+
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
test("active option is checked", () => {
|
|
@@ -10,12 +10,13 @@ import * as ToggleStories from "./Toggle.stories";
|
|
|
10
10
|
## Toggle Usage
|
|
11
11
|
|
|
12
12
|
```jsx
|
|
13
|
-
import React from "react";
|
|
13
|
+
import React, { useState } from "react";
|
|
14
14
|
|
|
15
15
|
import { Toggle } from "@cerebruminc/cerebellum";
|
|
16
16
|
|
|
17
|
-
const Example = (
|
|
18
|
-
|
|
17
|
+
const Example = () => {
|
|
18
|
+
const [active, setActive] = useState(false);
|
|
19
|
+
return <Toggle active={active} onToggle={() => setActive((prev) => !prev)} aria-label="Dark mode" />;
|
|
19
20
|
};
|
|
20
21
|
```
|
|
21
22
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { colors } from "../../const/colors";
|
|
4
|
-
import { BodyMEmphasis } from "../../const/text";
|
|
4
|
+
import { BodyMEmphasis, BodySPrimary } from "../../const/text";
|
|
5
5
|
import { Toggle } from "./Toggle";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A simple toggle switch. Please note that this is not an `<input>` element, and will not work with some form generators. `ToggleItem` and `ToggleGroup` both use `<input>` elements.
|
|
9
9
|
|
|
10
|
+
When `Toggle` is interactive, provide either `aria-label` or `aria-labelledby`. A wrapping `<label>` does not name a custom `role="switch"` element.
|
|
11
|
+
|
|
10
12
|
**Note:** You must handle the active state externally.
|
|
11
13
|
*/
|
|
12
14
|
const meta: Meta<typeof Toggle> = { component: Toggle, title: "components/Toggles/Toggle" };
|
|
@@ -35,12 +37,30 @@ export const WithChildren: Story = {
|
|
|
35
37
|
},
|
|
36
38
|
};
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Use `aria-labelledby` when the visible label is rendered separately from the custom switch element.
|
|
42
|
+
*/
|
|
43
|
+
export const WithSeparateLabel: Story = {
|
|
44
|
+
render: (args) => (
|
|
45
|
+
<div style={{ alignItems: "center", display: "flex", gap: "12px" }}>
|
|
46
|
+
<BodySPrimary id="toggle-separate-label">High contrast theme</BodySPrimary>
|
|
47
|
+
<Toggle {...args} aria-labelledby="toggle-separate-label" />
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
args: {
|
|
51
|
+
active: true,
|
|
52
|
+
disabled: false,
|
|
53
|
+
onToggle: () => console.log("ToggleClick"),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
38
57
|
/**
|
|
39
58
|
* You can override the theme if you need to. Make sure you know what you're doing, or something awful could happnen:
|
|
40
59
|
*/
|
|
41
60
|
export const OverrideTheTheme: Story = {
|
|
42
61
|
args: {
|
|
43
62
|
active: true,
|
|
63
|
+
"aria-label": "Toggle switch",
|
|
44
64
|
disabled: false,
|
|
45
65
|
onToggle: () => console.log("ToggleClick"),
|
|
46
66
|
knobColor: colors.RED_10,
|
|
@@ -38,6 +38,17 @@ describe("Toggle", () => {
|
|
|
38
38
|
expect(toggle).toBeInTheDocument();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test("uses aria-labelledby when provided", () => {
|
|
42
|
+
render(
|
|
43
|
+
<>
|
|
44
|
+
<span id="dark-mode-label">Dark mode</span>
|
|
45
|
+
<ThemedToggle active onToggle={jest.fn()} aria-labelledby="dark-mode-label" />
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
const toggle = screen.getByRole("switch", { name: "Dark mode" });
|
|
49
|
+
expect(toggle).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
41
52
|
test("toggles on Space key press", async () => {
|
|
42
53
|
const mockOnToggle = jest.fn();
|
|
43
54
|
render(<ThemedToggle active onToggle={mockOnToggle} />);
|
|
@@ -63,4 +74,10 @@ describe("Toggle", () => {
|
|
|
63
74
|
await expect(() => userEvent.click(toggle)).rejects.toThrow();
|
|
64
75
|
expect(mockOnClick).toHaveBeenCalledTimes(0);
|
|
65
76
|
});
|
|
77
|
+
|
|
78
|
+
test("renders as decorative when onToggle is not provided", () => {
|
|
79
|
+
render(<ThemedToggle active />);
|
|
80
|
+
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
|
|
81
|
+
expect(screen.getByTestId("toggle-component")).toHaveAttribute("aria-hidden", "true");
|
|
82
|
+
});
|
|
66
83
|
});
|
|
@@ -8,6 +8,7 @@ import { ToggleType } from "./types";
|
|
|
8
8
|
export const Toggle: FC<ToggleType> = ({
|
|
9
9
|
active,
|
|
10
10
|
"aria-label": ariaLabel,
|
|
11
|
+
"aria-labelledby": ariaLabelledBy,
|
|
11
12
|
clickPadding = 15,
|
|
12
13
|
children,
|
|
13
14
|
disabled,
|
|
@@ -24,6 +25,8 @@ export const Toggle: FC<ToggleType> = ({
|
|
|
24
25
|
trackHeight,
|
|
25
26
|
width,
|
|
26
27
|
}) => {
|
|
28
|
+
const isInteractive = !!onToggle;
|
|
29
|
+
|
|
27
30
|
return (
|
|
28
31
|
<ClickPadding
|
|
29
32
|
data-testid="toggle-component"
|
|
@@ -41,11 +44,13 @@ export const Toggle: FC<ToggleType> = ({
|
|
|
41
44
|
}
|
|
42
45
|
}}
|
|
43
46
|
$disabled={disabled}
|
|
44
|
-
role="switch"
|
|
45
|
-
aria-checked={active}
|
|
46
|
-
aria-disabled={disabled}
|
|
47
|
-
aria-
|
|
48
|
-
|
|
47
|
+
role={isInteractive ? "switch" : undefined}
|
|
48
|
+
aria-checked={isInteractive ? active : undefined}
|
|
49
|
+
aria-disabled={isInteractive ? disabled : undefined}
|
|
50
|
+
aria-hidden={isInteractive ? undefined : true}
|
|
51
|
+
aria-label={isInteractive ? ariaLabel : undefined}
|
|
52
|
+
aria-labelledby={isInteractive ? ariaLabelledBy : undefined}
|
|
53
|
+
tabIndex={!isInteractive || disabled ? -1 : 0}
|
|
49
54
|
>
|
|
50
55
|
<Base disabled={disabled} $knobSize={knobSize} $trackHeight={trackHeight} $width={width}>
|
|
51
56
|
<Knob $active={active} $knobSize={knobSize} $width={width}>
|
|
@@ -3,8 +3,10 @@ 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
|
|
6
|
+
/** Accessible label for an interactive toggle switch. Required when Toggle is used without a separate visible label element. */
|
|
7
7
|
"aria-label"?: string;
|
|
8
|
+
/** Id of the visible label for an interactive toggle switch. Prefer this when the label text is already rendered elsewhere. */
|
|
9
|
+
"aria-labelledby"?: string;
|
|
8
10
|
children?: React.ReactNode;
|
|
9
11
|
/** Clickable space added around Toggle, in pixels. Pass `0` to disable */
|
|
10
12
|
clickPadding?: number | string;
|
|
@@ -11,8 +11,9 @@ describe("ToggleItem", () => {
|
|
|
11
11
|
const text = "Unique ToggleItem text";
|
|
12
12
|
render(<ThemedToggleItem active={true} value={text} onToggle={jest.fn()} />);
|
|
13
13
|
|
|
14
|
-
const toggle = screen.
|
|
14
|
+
const toggle = screen.getByRole("checkbox", { name: text });
|
|
15
15
|
expect(toggle).toBeInTheDocument();
|
|
16
|
+
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
test("disabled checkbox is not clickable", async () => {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { act } from "react-dom/test-utils";
|
|
4
|
+
import { contrast } from "../../helpers/accessibility";
|
|
4
5
|
import { noop } from "../../helpers/utils";
|
|
5
|
-
import { FileTypeEnum } from "../../sharedTypes/enums";
|
|
6
6
|
import { withTheme } from "../../hocs/withTheme";
|
|
7
|
+
import { FileTypeEnum } from "../../sharedTypes/enums";
|
|
8
|
+
import { cerebellumTheme } from "../../theme";
|
|
7
9
|
import { UploadDropzone } from "./UploadDropzone";
|
|
8
10
|
|
|
9
11
|
const ThemedUploadDropzone = withTheme(UploadDropzone);
|
|
@@ -70,3 +72,40 @@ describe("UploadDropzone", () => {
|
|
|
70
72
|
expect(await screen.findByText("ping.json - Invalid file type")).toBeInTheDocument();
|
|
71
73
|
});
|
|
72
74
|
});
|
|
75
|
+
|
|
76
|
+
describe("UploadDropzone color contrast (WCAG 2.1 AA)", () => {
|
|
77
|
+
const WCAG_AA_TEXT = 4.5;
|
|
78
|
+
|
|
79
|
+
// Source directly from the theme so these tests catch regressions in
|
|
80
|
+
// both the token values and the theme wiring (e.g. textColor, secondaryText).
|
|
81
|
+
const textColor = cerebellumTheme.uploadDropzone.textColor;
|
|
82
|
+
|
|
83
|
+
const colorFamilyPairs = [
|
|
84
|
+
{
|
|
85
|
+
family: "Purple",
|
|
86
|
+
accent: cerebellumTheme.colorFamilies.PURPLE.secondaryText!,
|
|
87
|
+
background: cerebellumTheme.colorFamilies.PURPLE.light,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
family: "Blue",
|
|
91
|
+
accent: cerebellumTheme.colorFamilies.BLUE.secondaryText!,
|
|
92
|
+
background: cerebellumTheme.colorFamilies.BLUE.light,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
family: "Red",
|
|
96
|
+
accent: cerebellumTheme.colorFamilies.RED.secondaryText!,
|
|
97
|
+
background: cerebellumTheme.colorFamilies.RED.light,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
test.each(colorFamilyPairs)("$family: body text (COOL_GREY_70) achieves >= 4.5:1 contrast against light background", ({ background }) => {
|
|
102
|
+
expect(contrast(textColor, background)).toBeGreaterThanOrEqual(WCAG_AA_TEXT);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test.each(colorFamilyPairs)(
|
|
106
|
+
"$family: accent color (secondaryText) achieves >= 4.5:1 contrast against light background",
|
|
107
|
+
({ accent, background }) => {
|
|
108
|
+
expect(contrast(accent, background)).toBeGreaterThanOrEqual(WCAG_AA_TEXT);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React, { FC, useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useTheme } from "styled-components";
|
|
3
3
|
|
|
4
|
+
import { colors } from "../../const/colors";
|
|
4
5
|
import { CloseCircle, File, FileComplete, FileError as FileErrorIcon, UploadDocument } from "../Icons";
|
|
5
6
|
import { TextLink } from "../TextLink";
|
|
6
|
-
import { colors } from "../../const/colors";
|
|
7
7
|
import {
|
|
8
8
|
DropZoneContainer,
|
|
9
9
|
DropzoneBox,
|
|
@@ -22,13 +22,13 @@ import {
|
|
|
22
22
|
import { DropzoneColorFamilyEnum, FileError, UploadDropzoneType } from "./types";
|
|
23
23
|
|
|
24
24
|
import { FileRejection, useDropzone } from "react-dropzone";
|
|
25
|
-
import { Truncate } from "../Truncate";
|
|
26
25
|
import { formatBytesToString } from "../../helpers/numbers";
|
|
27
26
|
import useMeasure from "../../hooks/useMeasure";
|
|
28
27
|
import { showLabel } from "../../sharedHelpers/showLabel";
|
|
29
28
|
import { Asterisk, LabelBox, ValidationText } from "../../sharedStyle/Inputs";
|
|
30
29
|
import { TextStyleEnum } from "../../sharedTypes/enums";
|
|
31
30
|
import { ASTERISK_OFFSET_SPACE } from "../Input/helpers";
|
|
31
|
+
import { Truncate } from "../Truncate";
|
|
32
32
|
import { TextVariantEnum, Typography } from "../Typography";
|
|
33
33
|
import { createAcceptObject, defaultDescripion } from "./helpers";
|
|
34
34
|
|
|
@@ -57,6 +57,7 @@ export const UploadDropzone: FC<UploadDropzoneType> = ({
|
|
|
57
57
|
const theme = useTheme();
|
|
58
58
|
const { colorFamilies } = theme;
|
|
59
59
|
const colorGroup = colorFamilyOverride || colorFamilies[dropzoneColorFamily || DropzoneColorFamilyEnum.Purple];
|
|
60
|
+
const accentColor = colorGroup.secondaryText || colorGroup.main;
|
|
60
61
|
const acceptObject = createAcceptObject(acceptedFileTypes);
|
|
61
62
|
|
|
62
63
|
const [errorSummary, setErrorSummary] = useState<Array<FileError>>([]);
|
|
@@ -186,7 +187,7 @@ export const UploadDropzone: FC<UploadDropzoneType> = ({
|
|
|
186
187
|
{displayFileInline && !multipleFileUploads && errorSummary?.[0] ? (
|
|
187
188
|
<InlineFileItemBox>
|
|
188
189
|
<InlineIconBox>
|
|
189
|
-
{errorSummary.length > 0 ? <FileErrorIcon fill={theme.input.failColor} /> : <FileComplete fill={
|
|
190
|
+
{errorSummary.length > 0 ? <FileErrorIcon fill={theme.input.failColor} /> : <FileComplete fill={accentColor} />}
|
|
190
191
|
</InlineIconBox>
|
|
191
192
|
<InlineFileTextBox>
|
|
192
193
|
<Truncate>{errorSummary?.[0]?.name}</Truncate>
|
|
@@ -210,12 +211,11 @@ export const UploadDropzone: FC<UploadDropzoneType> = ({
|
|
|
210
211
|
<UploadIconBox>
|
|
211
212
|
<UploadDocument
|
|
212
213
|
boxColor={disabled ? theme?.uploadDropzone?.disabledColorLight : colorGroup.medium}
|
|
213
|
-
fill={disabled ? theme.uploadDropzone.disabledColorDark :
|
|
214
|
+
fill={disabled ? theme.uploadDropzone.disabledColorDark : accentColor}
|
|
214
215
|
/>
|
|
215
216
|
</UploadIconBox>
|
|
216
217
|
<TextSection $disabled={disabled} $isDragging={isDragActive} $themeOverride={themeOverride}>
|
|
217
|
-
{description ||
|
|
218
|
-
defaultDescripion(disabled ? theme.uploadDropzone.disabledColorText : colorGroup.hover, acceptObject, disabled)}
|
|
218
|
+
{description || defaultDescripion(disabled ? theme.uploadDropzone.disabledColorText : accentColor, acceptObject, disabled)}
|
|
219
219
|
</TextSection>
|
|
220
220
|
</>
|
|
221
221
|
)}
|
|
@@ -237,7 +237,7 @@ export const UploadDropzone: FC<UploadDropzoneType> = ({
|
|
|
237
237
|
{fileList.map((item, idx) => (
|
|
238
238
|
<FileItemBox key={`fileitem_${item?.name}_${idx}`}>
|
|
239
239
|
<ItemIconBox>
|
|
240
|
-
<File fill={
|
|
240
|
+
<File fill={accentColor} />
|
|
241
241
|
</ItemIconBox>
|
|
242
242
|
<Truncate>{item.name}</Truncate>
|
|
243
243
|
<RightAligned>
|
package/src/theme.ts
CHANGED
|
@@ -806,6 +806,7 @@ export const cerebellumTheme: DefaultTheme = {
|
|
|
806
806
|
main: colors.PURPLE_55,
|
|
807
807
|
hover: colors.PURPLE_70,
|
|
808
808
|
text: colors.WHITE,
|
|
809
|
+
secondaryText: colors.PURPLE_75,
|
|
809
810
|
},
|
|
810
811
|
RED: {
|
|
811
812
|
light: colors.RED_5,
|
|
@@ -1332,7 +1333,7 @@ export const cerebellumTheme: DefaultTheme = {
|
|
|
1332
1333
|
listDeleteColor: colors.BLUE_100,
|
|
1333
1334
|
minHeight: 180,
|
|
1334
1335
|
paddingString: "37px 26px 32px 26px",
|
|
1335
|
-
textColor:
|
|
1336
|
+
textColor: colors.COOL_GREY_70,
|
|
1336
1337
|
},
|
|
1337
1338
|
userCard: {
|
|
1338
1339
|
avatarBackgroundColor: colors.BLUE_55,
|