@cerebruminc/cerebellum 17.0.2 → 17.1.0
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 +13 -0
- package/package.json +4 -4
- package/src/components/CheckboxGroup/CheckboxGroup.test.tsx +29 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +2 -1
- package/src/components/CheckboxGroup/types.ts +3 -0
- package/src/components/ClickOutHandler/ClickOutHandler.tsx +1 -1
- package/src/components/DatePicker/DatePickerStyles.tsx +3 -1
- package/src/components/DescriptiveSwitchMenu/types.ts +1 -1
- package/src/components/DigitalInput/DigitalInput.stories.tsx +6 -6
- package/src/components/Form/Form.test.tsx +28 -0
- package/src/components/Form/fields/CheckboxGroupField/CheckboxGroupField.tsx +1 -0
- package/src/components/Form/fields/RadioGroupField/RadioGroupField.tsx +1 -0
- package/src/components/Form/fields/ToggleGroupField/ToggleGroupField.tsx +1 -0
- package/src/components/Form/types.ts +1 -1
- package/src/components/InlineInput/InlineInput.tsx +1 -0
- package/src/components/InlineInput/types.ts +1 -1
- package/src/components/Input/Input.test.tsx +28 -0
- package/src/components/RadioGroup/RadioGroup.test.tsx +29 -0
- package/src/components/RadioGroup/RadioGroup.tsx +2 -1
- package/src/components/RadioGroup/types.ts +3 -0
- package/src/components/SearchMenu/types.ts +1 -1
- package/src/components/Stepper/showFooterText.tsx +1 -1
- package/src/components/Stepper/types.ts +1 -1
- package/src/components/Table/EditCell.tsx +1 -1
- package/src/components/Table/types.ts +3 -3
- package/src/components/Textarea/InlineTextarea.tsx +1 -0
- package/src/components/Textarea/Textarea.test.tsx +25 -0
- package/src/components/Textarea/types.ts +1 -1
- package/src/components/ToggleGroup/ToggleGroup.test.tsx +29 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +2 -1
- package/src/components/ToggleGroup/types.ts +3 -0
- package/src/components/Tooltip/types.ts +1 -1
- package/src/components/TooltipLabel/types.ts +1 -1
- package/src/components/Typography/Typography.tsx +1 -1
- package/src/components/Typography/types.ts +1 -1
- package/src/configuredComponents/DescriptiveDropdownInput/DescriptiveDropdownInput.tsx +1 -1
- package/src/configuredComponents/InlineDescriptiveDropdown/InlineDescriptiveDropdown.tsx +1 -1
- package/src/configuredComponents/InlineDropdown/types.ts +1 -1
- package/src/hocs/PropsChecker.tsx +1 -1
- package/src/hooks/usePrevious.ts +1 -1
- package/src/sharedStyle/Inputs.tsx +3 -1
- package/src/sharedTypes/types.ts +1 -1
- package/tsconfig.json +2 -1
- package/tsconfig.test.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# react-component-lib-boilerplate
|
|
2
2
|
|
|
3
|
+
## [17.1.0](https://github.com/cerebruminc/cerebellum/compare/v17.0.2...v17.1.0) (2026-05-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **form:** add role="alert" to ValidationText for screen reader announcements ([6fb946d](https://github.com/cerebruminc/cerebellum/commit/6fb946dbdfbc6ed0ecea291cabc7b4a62bf3e99f))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* replace JSX.Element with React.JSX.Element across type definitions ([839ce02](https://github.com/cerebruminc/cerebellum/commit/839ce0238d74e1f3d20d847b87a2c4a60288e30e))
|
|
14
|
+
* resolve TypeScript errors after React 19 types upgrade ([c0b49ce](https://github.com/cerebruminc/cerebellum/commit/c0b49ce10e7be7bbedd7d89d60deef5a1b848774))
|
|
15
|
+
|
|
3
16
|
## [17.0.2](https://github.com/cerebruminc/cerebellum/compare/v17.0.1...v17.0.2) (2026-05-18)
|
|
4
17
|
|
|
5
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerebruminc/cerebellum",
|
|
3
|
-
"version": "17.0
|
|
3
|
+
"version": "17.1.0",
|
|
4
4
|
"description": "Cerebrum's React Component Library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"clean-storybook": "rm -rf docs",
|
|
21
21
|
"lint": "biome check ./src",
|
|
22
22
|
"lint:fix": "biome check ./src --write",
|
|
23
|
-
"tsc": "
|
|
23
|
+
"tsc": "NODE_OPTIONS=--max-old-space-size=8192 tsc",
|
|
24
24
|
"chromatic": "chromatic",
|
|
25
25
|
"chromatic:ci": "chromatic --exit-zero-on-changes"
|
|
26
26
|
},
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"@testing-library/user-event": "^14.0.0",
|
|
78
78
|
"@types/jest": "^29.5.12",
|
|
79
79
|
"@types/jscodeshift": "^0.12.0",
|
|
80
|
-
"@types/react": "^
|
|
81
|
-
"@types/react-dom": "^
|
|
80
|
+
"@types/react": "^19.2.14",
|
|
81
|
+
"@types/react-dom": "^19.2.3",
|
|
82
82
|
"babel-jest": "^29.6.1",
|
|
83
83
|
"babel-loader": "^10.0.0",
|
|
84
84
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
|
@@ -43,6 +43,35 @@ describe("CheckboxGroup", () => {
|
|
|
43
43
|
expect(validationTextNode).toBeInTheDocument();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
47
|
+
const validationMessage = "Selection required";
|
|
48
|
+
render(
|
|
49
|
+
<ThemedCheckboxGroup onClick={jest.fn()} activeValues={[]} checkboxes={[]} label="foo" validationMessage={validationMessage} showValidationMessage />
|
|
50
|
+
);
|
|
51
|
+
const alert = screen.getByRole("alert");
|
|
52
|
+
expect(alert).toHaveTextContent(validationMessage);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
56
|
+
const validationMessage = "Selection required";
|
|
57
|
+
render(<ThemedCheckboxGroup onClick={jest.fn()} activeValues={[]} checkboxes={[]} label="foo" validationMessage={validationMessage} />);
|
|
58
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("group has aria-invalid when showValidationMessage is true", () => {
|
|
62
|
+
render(
|
|
63
|
+
<ThemedCheckboxGroup onClick={jest.fn()} activeValues={[]} checkboxes={[]} label="foo" validationMessage="Error" showValidationMessage />
|
|
64
|
+
);
|
|
65
|
+
const group = screen.getByRole("group");
|
|
66
|
+
expect(group).toHaveAttribute("aria-invalid", "true");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("group does not have aria-invalid when showValidationMessage is false", () => {
|
|
70
|
+
render(<ThemedCheckboxGroup onClick={jest.fn()} activeValues={[]} checkboxes={[]} label="foo" />);
|
|
71
|
+
const group = screen.getByRole("group");
|
|
72
|
+
expect(group).not.toHaveAttribute("aria-invalid");
|
|
73
|
+
});
|
|
74
|
+
|
|
46
75
|
test("renders multiple checkboxes text", () => {
|
|
47
76
|
const text = "Multiple CheckboxGroup text";
|
|
48
77
|
const labels = ["Test label 1", "Test label 2", "Test label 3"];
|
|
@@ -34,6 +34,7 @@ export const CheckboxGroup: FC<CheckboxGroupType> = ({
|
|
|
34
34
|
noBottomPadding = false,
|
|
35
35
|
onClick,
|
|
36
36
|
required,
|
|
37
|
+
hasError,
|
|
37
38
|
showValidationMessage = false,
|
|
38
39
|
titleColor,
|
|
39
40
|
validationMessage,
|
|
@@ -44,7 +45,7 @@ export const CheckboxGroup: FC<CheckboxGroupType> = ({
|
|
|
44
45
|
const disabledTextColor = disabledLabelColor || theme.text.disabledColor;
|
|
45
46
|
|
|
46
47
|
return (
|
|
47
|
-
<CheckboxGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={checkboxGroupWidth} role="group" aria-required={required ? "true" : undefined}>
|
|
48
|
+
<CheckboxGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={checkboxGroupWidth} role="group" aria-invalid={(hasError ?? showValidationMessage) ? "true" : undefined} aria-required={required ? "true" : undefined}>
|
|
48
49
|
{label ? (
|
|
49
50
|
typeof label === "string" ? (
|
|
50
51
|
<BodyXSEmphasis $textColor={disableGroup ? disabledTextColor : titleColor} data-sentry-unmask>
|
|
@@ -31,6 +31,9 @@ export interface CheckboxGroupType extends Omit<CheckboxItemType, "active" | "di
|
|
|
31
31
|
onClick: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
32
32
|
/** Adds an asterisk to the UI when there is also a `label` */
|
|
33
33
|
required?: boolean;
|
|
34
|
+
/** Sets aria-invalid on the group independently of showValidationMessage. Useful when the inline
|
|
35
|
+
* message is suppressed (e.g. leftLabels Form layout) but the field is still in an error state. */
|
|
36
|
+
hasError?: boolean;
|
|
34
37
|
/** Displays an error message under the last checkbox */
|
|
35
38
|
showValidationMessage?: boolean;
|
|
36
39
|
/** The title color. THEME PROP: default is colors.COOL_GREY_80 */
|
|
@@ -3,7 +3,7 @@ import { Wrapper } from "./ClickOutHandlerStyles";
|
|
|
3
3
|
import { ClickOutHandlerType } from "./types";
|
|
4
4
|
|
|
5
5
|
export const ClickOutHandler: FC<ClickOutHandlerType> = ({ children, handleClickAway, borderRadius }: ClickOutHandlerType) => {
|
|
6
|
-
const wrapperRef = useRef<any>();
|
|
6
|
+
const wrapperRef = useRef<any>(null);
|
|
7
7
|
|
|
8
8
|
useEffect(() => {
|
|
9
9
|
function handleClickOutside(event: Event) {
|
|
@@ -119,7 +119,9 @@ export const HelperText = styled.p<HelperTextProps>`
|
|
|
119
119
|
font-weight: 400;
|
|
120
120
|
margin-top: 10px;
|
|
121
121
|
`;
|
|
122
|
-
export const ValidationText = styled.p<ValidationTextProps
|
|
122
|
+
export const ValidationText = styled.p.attrs<ValidationTextProps>(({ $showValidationMessage }) => ({
|
|
123
|
+
role: $showValidationMessage ? "alert" : undefined,
|
|
124
|
+
}))<ValidationTextProps>`
|
|
123
125
|
color: ${(props) =>
|
|
124
126
|
props.disabled ? props.$disabledColor || props.theme.datePicker?.disabledColor : props.$failColor || props.theme.datePicker?.failColor};
|
|
125
127
|
display: inline-block;
|
|
@@ -18,9 +18,9 @@ export const BasicExample: Story = {
|
|
|
18
18
|
return (
|
|
19
19
|
<DigitalInput
|
|
20
20
|
{...args}
|
|
21
|
-
onChange={(
|
|
22
|
-
args.onChange?.(
|
|
23
|
-
updateArgs({ value });
|
|
21
|
+
onChange={(arg) => {
|
|
22
|
+
args.onChange?.(arg);
|
|
23
|
+
updateArgs({ value: arg.value });
|
|
24
24
|
}}
|
|
25
25
|
/>
|
|
26
26
|
);
|
|
@@ -72,9 +72,9 @@ export const OnComplete: Story = {
|
|
|
72
72
|
<>
|
|
73
73
|
<DigitalInput
|
|
74
74
|
{...args}
|
|
75
|
-
onChange={(
|
|
76
|
-
args.onChange?.(
|
|
77
|
-
updateArgs({ value });
|
|
75
|
+
onChange={(arg) => {
|
|
76
|
+
args.onChange?.(arg);
|
|
77
|
+
updateArgs({ value: arg.value });
|
|
78
78
|
}}
|
|
79
79
|
onComplete={(value) => {
|
|
80
80
|
DialogHandler.show({
|
|
@@ -144,6 +144,34 @@ describe("Form", () => {
|
|
|
144
144
|
await waitFor(() => expect(screen.getByText(errorMessage)).toBeInTheDocument());
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
test("validation error has role='alert' for screen reader announcement", async () => {
|
|
148
|
+
const errorMessage = "This field is required";
|
|
149
|
+
const submitButtonText = "Submit";
|
|
150
|
+
const fields: SingleOrMultipleFieldType[] = [{ fieldLabel: "field 1", required: true, requiredMessage: errorMessage, name: "field 1" }];
|
|
151
|
+
render(<ThemedForm submitButtonText={submitButtonText} title="foo" fields={fields} submit={jest.fn()} />);
|
|
152
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
153
|
+
const button = screen.getByText(submitButtonText);
|
|
154
|
+
await waitFor(() => userEvent.click(button));
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
const alert = screen.getByRole("alert");
|
|
157
|
+
expect(alert).toHaveTextContent(errorMessage);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("leftLabels validation error has role='alert' for screen reader announcement", async () => {
|
|
162
|
+
const errorMessage = "This field is required";
|
|
163
|
+
const submitButtonText = "Submit";
|
|
164
|
+
const fields: SingleOrMultipleFieldType[] = [{ fieldLabel: "field 1", required: true, requiredMessage: errorMessage, name: "field 1" }];
|
|
165
|
+
render(<ThemedForm leftLabels submitButtonText={submitButtonText} title="foo" fields={fields} submit={jest.fn()} />);
|
|
166
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
167
|
+
const button = screen.getByText(submitButtonText);
|
|
168
|
+
await waitFor(() => userEvent.click(button));
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
const alert = screen.getByRole("alert");
|
|
171
|
+
expect(alert).toHaveTextContent(errorMessage);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
147
175
|
test("disableSubmitUntilValid disables submit button if the form is not completed", async () => {
|
|
148
176
|
const onSubmit = jest.fn();
|
|
149
177
|
const fields = [{ fieldLabel: "field 1", name: "bar", required: true }];
|
|
@@ -69,6 +69,7 @@ export const RadioGroupField: FC<RadioGroupFieldProps> = ({
|
|
|
69
69
|
}}
|
|
70
70
|
radioLabel={leftLabels ? undefined : fieldLabel || name}
|
|
71
71
|
required={required && !disabled}
|
|
72
|
+
hasError={showValidationMessage}
|
|
72
73
|
showValidationMessage={!leftLabels && showValidationMessage}
|
|
73
74
|
validationMessage={validationMessage}
|
|
74
75
|
/>
|
|
@@ -68,6 +68,7 @@ export const ToggleGroupField: FC<ToggleGroupFieldProps> = ({
|
|
|
68
68
|
}
|
|
69
69
|
}}
|
|
70
70
|
required={required && !disabled}
|
|
71
|
+
hasError={showValidationMessage}
|
|
71
72
|
showValidationMessage={!leftLabels && showValidationMessage}
|
|
72
73
|
toggleGroupLabel={leftLabels ? undefined : fieldLabel || name}
|
|
73
74
|
validationMessage={validationMessage}
|
|
@@ -52,7 +52,7 @@ export interface FormType<FormData extends Record<string, any> = Record<string,
|
|
|
52
52
|
/** Hides the submit button. Not necessary for `submitOnBlur`, but useful if you want to have an external submit button. */
|
|
53
53
|
hideSubmitButton?: boolean;
|
|
54
54
|
/** Passthrough ref to the form. Allows for external submit and other fun */
|
|
55
|
-
innerRef?: RefObject<FormikProps<FormData
|
|
55
|
+
innerRef?: RefObject<FormikProps<FormData> | null>;
|
|
56
56
|
/** Aligns the titles to the left and the inputs to the right */
|
|
57
57
|
leftLabels?: boolean;
|
|
58
58
|
/** The width reserved for the right validation space when in left label mode, in pixels */
|
|
@@ -191,6 +191,7 @@ export const InlineInput: FC<InlineInputType> = ({
|
|
|
191
191
|
$showValidationMessage={showValidationMessage}
|
|
192
192
|
$sidePadding={sidePadding}
|
|
193
193
|
step={type === "number" ? step : undefined}
|
|
194
|
+
aria-invalid={showValidationMessage ? "true" : undefined}
|
|
194
195
|
aria-required={required ? "true" : undefined}
|
|
195
196
|
required={required}
|
|
196
197
|
role="textbox"
|
|
@@ -22,7 +22,7 @@ export interface InlineInputType {
|
|
|
22
22
|
/** Sets the width, in pixels. Default is 100%, with a 700px max width. */
|
|
23
23
|
inputWidth?: number;
|
|
24
24
|
/** Used to pass a ref to the InlineInput component */
|
|
25
|
-
innerRef?: RefObject<HTMLInputElement>;
|
|
25
|
+
innerRef?: RefObject<HTMLInputElement | null>;
|
|
26
26
|
/** Left aligned icon used to further emphasize the Input Label. Accepts a Cerebellum Icon component directly, or JSX */
|
|
27
27
|
LeadingIcon?: IconType | FC;
|
|
28
28
|
/** HTML inputmode attribute. Used to determine keyboard type only */
|
|
@@ -43,6 +43,34 @@ describe("Input", () => {
|
|
|
43
43
|
expect(asterisk).toBeInTheDocument();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
// ------ Screen reader accessibility tests ------
|
|
47
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
48
|
+
const validationMessage = "This field is required";
|
|
49
|
+
render(<ThemedInput value="" validationMessage={validationMessage} showValidationMessage />);
|
|
50
|
+
const alert = screen.getByRole("alert");
|
|
51
|
+
expect(alert).toHaveTextContent(validationMessage);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
55
|
+
const validationMessage = "This field is required";
|
|
56
|
+
render(<ThemedInput value="" validationMessage={validationMessage} />);
|
|
57
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("input has aria-invalid when showValidationMessage is true", () => {
|
|
61
|
+
const label = "Test input";
|
|
62
|
+
render(<ThemedInput value="" inputLabel={label} showValidationMessage validationMessage="Error" />);
|
|
63
|
+
const input = screen.getByLabelText(label);
|
|
64
|
+
expect(input).toHaveAttribute("aria-invalid", "true");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("input does not have aria-invalid when showValidationMessage is false", () => {
|
|
68
|
+
const label = "Test input";
|
|
69
|
+
render(<ThemedInput value="" inputLabel={label} />);
|
|
70
|
+
const input = screen.getByLabelText(label);
|
|
71
|
+
expect(input).not.toHaveAttribute("aria-invalid");
|
|
72
|
+
});
|
|
73
|
+
|
|
46
74
|
// ------ The rest of these tests are redundant with InlinInput's tests, but there's not much point deleting them
|
|
47
75
|
test("renders Input with the text present", () => {
|
|
48
76
|
const inputValue = "foo";
|
|
@@ -47,6 +47,35 @@ describe("RadioGroup", () => {
|
|
|
47
47
|
expect(validationMessage).toBeInTheDocument();
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
51
|
+
const text = "Selection required";
|
|
52
|
+
render(
|
|
53
|
+
<ThemedRadioGroup activeId="" onRadioClick={jest.fn()} radios={radioExamples} showValidationMessage validationMessage={text} name="test_radio" />
|
|
54
|
+
);
|
|
55
|
+
const alert = screen.getByRole("alert");
|
|
56
|
+
expect(alert).toHaveTextContent(text);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
60
|
+
const text = "Selection required";
|
|
61
|
+
render(<ThemedRadioGroup activeId="" onRadioClick={jest.fn()} radios={radioExamples} validationMessage={text} name="test_radio" />);
|
|
62
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("radiogroup has aria-invalid when showValidationMessage is true", () => {
|
|
66
|
+
render(
|
|
67
|
+
<ThemedRadioGroup activeId="" onRadioClick={jest.fn()} radios={radioExamples} showValidationMessage validationMessage="Error" name="test_radio" />
|
|
68
|
+
);
|
|
69
|
+
const group = screen.getByRole("radiogroup");
|
|
70
|
+
expect(group).toHaveAttribute("aria-invalid", "true");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("radiogroup does not have aria-invalid when showValidationMessage is false", () => {
|
|
74
|
+
render(<ThemedRadioGroup activeId="" onRadioClick={jest.fn()} radios={radioExamples} name="test_radio" />);
|
|
75
|
+
const group = screen.getByRole("radiogroup");
|
|
76
|
+
expect(group).not.toHaveAttribute("aria-invalid");
|
|
77
|
+
});
|
|
78
|
+
|
|
50
79
|
test("multiple radios test", () => {
|
|
51
80
|
const text = "Multiple RadioGroup text";
|
|
52
81
|
const onClick = jest.fn();
|
|
@@ -34,6 +34,7 @@ export const RadioGroup: FC<RadioGroupType> = ({
|
|
|
34
34
|
radioSize,
|
|
35
35
|
radios,
|
|
36
36
|
required,
|
|
37
|
+
hasError,
|
|
37
38
|
showValidationMessage = false,
|
|
38
39
|
titleColor,
|
|
39
40
|
labelGap,
|
|
@@ -57,7 +58,7 @@ export const RadioGroup: FC<RadioGroupType> = ({
|
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
return (
|
|
60
|
-
<RadioGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={radioGroupWidth} role="radiogroup" aria-required={required ? "true" : undefined}>
|
|
61
|
+
<RadioGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={radioGroupWidth} role="radiogroup" aria-invalid={(hasError ?? showValidationMessage) ? "true" : undefined} aria-required={required ? "true" : undefined}>
|
|
61
62
|
<LabelBox>
|
|
62
63
|
{showLabel()}
|
|
63
64
|
{required && <Asterisk $asteriskColor={asteriskColor}>*</Asterisk>}
|
|
@@ -33,6 +33,9 @@ export interface RadioGroupType extends Omit<RadioItemType, "active" | "disabled
|
|
|
33
33
|
radioGroupWidth?: number;
|
|
34
34
|
/** Adds an asterisk to the UI when there is also a `radioLabel` */
|
|
35
35
|
required?: boolean;
|
|
36
|
+
/** Sets aria-invalid on the group independently of showValidationMessage. Useful when the inline
|
|
37
|
+
* message is suppressed (e.g. leftLabels Form layout) but the field is still in an error state. */
|
|
38
|
+
hasError?: boolean;
|
|
36
39
|
/** Displays an error message under the last radio */
|
|
37
40
|
showValidationMessage?: boolean;
|
|
38
41
|
/** The title color. THEME PROP: default is colors.COOL_GREY_80 */
|
|
@@ -45,7 +45,7 @@ export interface SearchMenuType {
|
|
|
45
45
|
/** Search will include the middle of words */
|
|
46
46
|
innerSearch?: boolean;
|
|
47
47
|
/** Used to pass a ref to the SearchMenu parent element */
|
|
48
|
-
innerRef?: RefObject<HTMLSpanElement>;
|
|
48
|
+
innerRef?: RefObject<HTMLSpanElement | null>;
|
|
49
49
|
/** Is the SearchMenu visible? This is used to reset the search on open, and to prevent `onSelect` from firing - not to show/hide the menu. */
|
|
50
50
|
isVisible?: boolean;
|
|
51
51
|
/** The field in each option to use instead of `label` */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { FC } from "react";
|
|
2
2
|
import { BodyMEmphasis } from "../../const/text";
|
|
3
3
|
|
|
4
|
-
export const showFooterText = ({ footerText }: { footerText?: string | FC | JSX.Element }) => {
|
|
4
|
+
export const showFooterText = ({ footerText }: { footerText?: string | FC | React.JSX.Element }) => {
|
|
5
5
|
if (footerText) {
|
|
6
6
|
if (typeof footerText === "function") {
|
|
7
7
|
const ComponentLabel = footerText;
|
|
@@ -22,7 +22,7 @@ export interface StepperType {
|
|
|
22
22
|
/** A URL for an image. Adds a footer with this image in it. Works best with .pngs with a transparent background */
|
|
23
23
|
footerImage?: string;
|
|
24
24
|
/** Displays text in the footer */
|
|
25
|
-
footerText?: string | FC | JSX.Element;
|
|
25
|
+
footerText?: string | FC | React.JSX.Element;
|
|
26
26
|
/** Called when a step is clicked */
|
|
27
27
|
stepClick?: (id: string) => void;
|
|
28
28
|
/** The options to display in your steps */
|
|
@@ -40,7 +40,7 @@ export const EditCell: FC<EditCellProps> = ({
|
|
|
40
40
|
submitData,
|
|
41
41
|
}) => {
|
|
42
42
|
const usableInitialValue = isUsableInitialValue({ initialValue, editType });
|
|
43
|
-
const [value, setValue] = useState<string | number | boolean | JSX.Element | Array<string>>(
|
|
43
|
+
const [value, setValue] = useState<string | number | boolean | React.JSX.Element | Array<string>>(
|
|
44
44
|
usableInitialValue ? initialValue : setCorrectValueType({ initialValue, editType })
|
|
45
45
|
);
|
|
46
46
|
const previouslyVisible = usePrevious(isVisible);
|
|
@@ -91,9 +91,9 @@ export interface TableType {
|
|
|
91
91
|
/** This will enable a button in the no results state, and will be called when the button is clicked. Usually it will clear the filters or a similar action */
|
|
92
92
|
noResultsFunction?: () => void;
|
|
93
93
|
/** A header for the no results message */
|
|
94
|
-
noResultsHeader?: string | JSX.Element;
|
|
94
|
+
noResultsHeader?: string | React.JSX.Element;
|
|
95
95
|
/** The text for the no results message */
|
|
96
|
-
noResultsText?: string | JSX.Element;
|
|
96
|
+
noResultsText?: string | React.JSX.Element;
|
|
97
97
|
/** The text for the no results button. Useless unless `noResultsFunction` is passed */
|
|
98
98
|
noResultsButtonText?: string;
|
|
99
99
|
/** Will close menus & tooltips when this element scrolls. This must be supplied on mount. Defaults to `window` */
|
|
@@ -499,7 +499,7 @@ export interface EditCellProps {
|
|
|
499
499
|
rightPadding?: number;
|
|
500
500
|
roomForButtons?: boolean;
|
|
501
501
|
rowData: RowType;
|
|
502
|
-
setCellData: (value: string | number | boolean | JSX.Element | Array<string>) => void;
|
|
502
|
+
setCellData: (value: string | number | boolean | React.JSX.Element | Array<string>) => void;
|
|
503
503
|
setEditErrorTooltipPosition?: (position: PositionType) => void;
|
|
504
504
|
showError?: boolean;
|
|
505
505
|
submitData: (value: string | number) => void;
|
|
@@ -37,6 +37,7 @@ export const InlineTextarea: FC<InlineTextareaType> = ({
|
|
|
37
37
|
return (
|
|
38
38
|
<PositionWrapper $width={inputWidth}>
|
|
39
39
|
<TextareaElement
|
|
40
|
+
aria-invalid={showValidationMessage ? "true" : undefined}
|
|
40
41
|
id={htmlId}
|
|
41
42
|
ref={textareaRef}
|
|
42
43
|
$activeBorderColor={activeBorderColor}
|
|
@@ -78,6 +78,31 @@ describe("Textarea", () => {
|
|
|
78
78
|
expect(element).toBeInTheDocument();
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
82
|
+
const validationMessage = "This field is required";
|
|
83
|
+
render(<ThemedTextarea showValidationMessage validationMessage={validationMessage} value={"Foo"} />);
|
|
84
|
+
const alert = screen.getByRole("alert");
|
|
85
|
+
expect(alert).toHaveTextContent(validationMessage);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
89
|
+
const validationMessage = "This field is required";
|
|
90
|
+
render(<ThemedTextarea validationMessage={validationMessage} value={"Foo"} />);
|
|
91
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("textarea has aria-invalid when showValidationMessage is true", () => {
|
|
95
|
+
render(<ThemedTextarea showValidationMessage validationMessage="Error" value={"Foo"} />);
|
|
96
|
+
const textarea = screen.getByRole("textbox");
|
|
97
|
+
expect(textarea).toHaveAttribute("aria-invalid", "true");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("textarea does not have aria-invalid when showValidationMessage is false", () => {
|
|
101
|
+
render(<ThemedTextarea value={"Foo"} />);
|
|
102
|
+
const textarea = screen.getByRole("textbox");
|
|
103
|
+
expect(textarea).not.toHaveAttribute("aria-invalid");
|
|
104
|
+
});
|
|
105
|
+
|
|
81
106
|
test("helper text is visible when received as a prop", () => {
|
|
82
107
|
const helperText = "This is a helper text";
|
|
83
108
|
render(<ThemedTextarea helperText={helperText} value={"Foo"} />);
|
|
@@ -33,7 +33,7 @@ export interface InlineTextareaType {
|
|
|
33
33
|
/** Styles the Textarea as if it showing a validation message */
|
|
34
34
|
showValidationMessage?: boolean;
|
|
35
35
|
/** A ref object for the textarea element */
|
|
36
|
-
textareaRef?: RefObject<HTMLTextAreaElement>;
|
|
36
|
+
textareaRef?: RefObject<HTMLTextAreaElement | null>;
|
|
37
37
|
/** Sets the theme */
|
|
38
38
|
theme?: InputTheme;
|
|
39
39
|
/** Textarea value. This must always be defined. */
|
|
@@ -35,6 +35,35 @@ describe("ToggleGroup", () => {
|
|
|
35
35
|
expect(errorMessage).toBeInTheDocument();
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
test("validation message has role='alert' when showValidationMessage is true", () => {
|
|
39
|
+
const validationMessage = "Selection required";
|
|
40
|
+
render(
|
|
41
|
+
<ThemedToggleGroup showValidationMessage validationMessage={validationMessage} onToggle={jest.fn()} toggles={[]} activeValues={[]} />
|
|
42
|
+
);
|
|
43
|
+
const alert = screen.getByRole("alert");
|
|
44
|
+
expect(alert).toHaveTextContent(validationMessage);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("validation message does not have role='alert' when showValidationMessage is false", () => {
|
|
48
|
+
const validationMessage = "Selection required";
|
|
49
|
+
render(<ThemedToggleGroup validationMessage={validationMessage} onToggle={jest.fn()} toggles={[]} activeValues={[]} />);
|
|
50
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("group has aria-invalid when showValidationMessage is true", () => {
|
|
54
|
+
render(
|
|
55
|
+
<ThemedToggleGroup showValidationMessage validationMessage="Error" onToggle={jest.fn()} toggles={[]} activeValues={[]} />
|
|
56
|
+
);
|
|
57
|
+
const group = screen.getByRole("group");
|
|
58
|
+
expect(group).toHaveAttribute("aria-invalid", "true");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("group does not have aria-invalid when showValidationMessage is false", () => {
|
|
62
|
+
render(<ThemedToggleGroup toggleGroupLabel="label" onToggle={jest.fn()} toggles={[]} activeValues={[]} />);
|
|
63
|
+
const group = screen.getByRole("group");
|
|
64
|
+
expect(group).not.toHaveAttribute("aria-invalid");
|
|
65
|
+
});
|
|
66
|
+
|
|
38
67
|
test("multiple toggles test", () => {
|
|
39
68
|
const labels = ["Test label 1", "Test label 2", "Test label 3"];
|
|
40
69
|
const onToggle = jest.fn();
|
|
@@ -23,6 +23,7 @@ export const ToggleGroup: FC<ToggleGroupType> = (props) => {
|
|
|
23
23
|
noBottomPadding = false,
|
|
24
24
|
onToggle,
|
|
25
25
|
required,
|
|
26
|
+
hasError,
|
|
26
27
|
showValidationMessage = false,
|
|
27
28
|
toggles,
|
|
28
29
|
titleColor,
|
|
@@ -35,7 +36,7 @@ export const ToggleGroup: FC<ToggleGroupType> = (props) => {
|
|
|
35
36
|
const disabledTextColor = disabledLabelColor || theme.text.disabledColor;
|
|
36
37
|
|
|
37
38
|
return (
|
|
38
|
-
<ToggleGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={toggleGroupWidth} role="group" aria-required={required ? "true" : undefined}>
|
|
39
|
+
<ToggleGroupBase $labelGap={labelGap} $noBottomPadding={noBottomPadding} $width={toggleGroupWidth} role="group" aria-invalid={(hasError ?? showValidationMessage) ? "true" : undefined} aria-required={required ? "true" : undefined}>
|
|
39
40
|
{toggleGroupLabel ? (
|
|
40
41
|
typeof toggleGroupLabel === "string" ? (
|
|
41
42
|
<BodyXSEmphasis $textColor={disableGroup ? disabledTextColor : titleColor} data-sentry-unmask>
|
|
@@ -20,6 +20,9 @@ export interface ToggleGroupType extends Omit<ToggleItemType, "active" | "label"
|
|
|
20
20
|
onToggle?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
21
21
|
/** Adds an asterisk to the UI when there is also a `toggleGroupLabel` */
|
|
22
22
|
required?: boolean;
|
|
23
|
+
/** Sets aria-invalid on the group independently of showValidationMessage. Useful when the inline
|
|
24
|
+
* message is suppressed (e.g. leftLabels Form layout) but the field is still in an error state. */
|
|
25
|
+
hasError?: boolean;
|
|
23
26
|
/** Displays an error message under the last toggle */
|
|
24
27
|
showValidationMessage?: boolean;
|
|
25
28
|
/** The label for ToggleGroup. Not required, but strongly recommended. */
|
|
@@ -8,7 +8,7 @@ export interface TooltipType {
|
|
|
8
8
|
/** Wraps the component in ClickOutHandler and calls this on click out. Only useful if `clickable: true` */
|
|
9
9
|
clickOut?: () => void;
|
|
10
10
|
direction?: string;
|
|
11
|
-
innerRef?: RefObject<HTMLDivElement>;
|
|
11
|
+
innerRef?: RefObject<HTMLDivElement | null>;
|
|
12
12
|
offset?: number;
|
|
13
13
|
onOpen?: () => void;
|
|
14
14
|
onMouseout?: (event: MouseEventInit) => void;
|
|
@@ -13,7 +13,7 @@ export interface TooltipLabelType {
|
|
|
13
13
|
/** Will close the tooltip when this element scrolls. This must be supplied on mount. Defaults to `window` */
|
|
14
14
|
observeScrollRef?: RefObject<HTMLElement>;
|
|
15
15
|
/** Contents of the tooltip. This may be a string or a react component. */
|
|
16
|
-
tooltipText: string | FC | JSX.Element;
|
|
16
|
+
tooltipText: string | FC | React.JSX.Element;
|
|
17
17
|
/** Overrides the theme style. */
|
|
18
18
|
themeOverride?: ThemeOverride;
|
|
19
19
|
/** Width of the tooltip, in pixels. */
|
|
@@ -42,7 +42,7 @@ export const Typography: FC<TypographyProps> = ({
|
|
|
42
42
|
truncateLines,
|
|
43
43
|
variant,
|
|
44
44
|
}) => {
|
|
45
|
-
const textRef: RefObject<HTMLParagraphElement> = innerRef || useRef<HTMLParagraphElement>(null);
|
|
45
|
+
const textRef: RefObject<HTMLParagraphElement | null> = innerRef || useRef<HTMLParagraphElement>(null);
|
|
46
46
|
const payload = truncate ? (
|
|
47
47
|
<Truncate lines={truncateLines} onHover={onTruncateHover} onHoverOut={onTruncateHoverOut} sentryUnmask={sentryUnmask}>
|
|
48
48
|
{children}
|
|
@@ -30,7 +30,7 @@ export enum TextVariantEnum {
|
|
|
30
30
|
export interface TypographyProps {
|
|
31
31
|
children?: ReactNode;
|
|
32
32
|
/** Used to pass a ref into Typography */
|
|
33
|
-
innerRef?: RefObject<HTMLParagraphElement>;
|
|
33
|
+
innerRef?: RefObject<HTMLParagraphElement | null>;
|
|
34
34
|
/** The line-height, in pixels. If you need a non-pixel value, you'll need to extend the style and overwrite */
|
|
35
35
|
lineHeight?: number;
|
|
36
36
|
/** Truncate hover function. This will only be applied if `truncate` is passed. */
|
|
@@ -52,7 +52,7 @@ export const DescriptiveDropdownInput: FC<DescriptiveDropdownInputType> = ({
|
|
|
52
52
|
const [showMenu, setShowMenu] = useState(defaultOpen);
|
|
53
53
|
const [activeTags, setActiveTags] = useState<OptionType[] | undefined>();
|
|
54
54
|
const [taglessArray, setTaglessArray] = useState<string[]>([]);
|
|
55
|
-
const [buttonText, setButtonText] = useState<string | FC | JSX.Element>("");
|
|
55
|
+
const [buttonText, setButtonText] = useState<string | FC | React.JSX.Element>("");
|
|
56
56
|
const [targetRef, targetMeasure] = useMeasureWatchResize([showMenu]);
|
|
57
57
|
|
|
58
58
|
const menuWidth = themeOverride?.menuWidth;
|
|
@@ -41,7 +41,7 @@ export const InlineDescriptiveDropdown: FC<InlineDescriptiveDropdownType> = ({
|
|
|
41
41
|
const [activeId, setActiveId] = useState(defaultId);
|
|
42
42
|
const [showMenu, setShowMenu] = useState(defaultOpen);
|
|
43
43
|
const [taglessArray, setTaglessArray] = useState<string[]>([]);
|
|
44
|
-
const [buttonText, setButtonText] = useState<string | JSX.Element>("");
|
|
44
|
+
const [buttonText, setButtonText] = useState<string | React.JSX.Element>("");
|
|
45
45
|
const [targetRef, targetMeasure] = useMeasureWatchResize([showMenu]);
|
|
46
46
|
|
|
47
47
|
const menuWidth = themeOverride?.menuWidth || 397;
|
|
@@ -61,7 +61,7 @@ export interface InlineDropdownType {
|
|
|
61
61
|
/** Padding for your link. You may need to adjust this to get your TextLink to fit with the surrouning text. Default is `"4px 20px 3px 3px"` */
|
|
62
62
|
paddingString?: string;
|
|
63
63
|
/** The text for the button. Uses text components from the textStyle enum if a string */
|
|
64
|
-
text: string | JSX.Element;
|
|
64
|
+
text: string | React.JSX.Element;
|
|
65
65
|
/** The style for the button text */
|
|
66
66
|
textStyle?: TextStyleEnum;
|
|
67
67
|
/** Underlines the link */
|
|
@@ -81,7 +81,7 @@ function compFNSelection(compType: ComparaisonTypes) {
|
|
|
81
81
|
*/
|
|
82
82
|
export function ReactFnCompPropsChecker<T extends Props>(props: PropsCheckerProps<T>) {
|
|
83
83
|
const { children, childrenProps, compType = "SIMPLE", verbose } = props;
|
|
84
|
-
const oldPropsRef = React.useRef<T>();
|
|
84
|
+
const oldPropsRef = React.useRef<T | undefined>(undefined);
|
|
85
85
|
React.useEffect(() => {
|
|
86
86
|
const oldProps = oldPropsRef.current;
|
|
87
87
|
if (oldProps === undefined) {
|
package/src/hooks/usePrevious.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
|
|
4
4
|
export function usePrevious(value: any) {
|
|
5
|
-
const ref = useRef();
|
|
5
|
+
const ref = useRef(undefined);
|
|
6
6
|
useEffect(() => {
|
|
7
7
|
ref.current = value; //assign the value of ref to the argument
|
|
8
8
|
}, [value]); //this code will run when the value of 'value' changes
|
|
@@ -221,7 +221,9 @@ export const TrailingText = styled.p<TrailingTextProps>`
|
|
|
221
221
|
right: ${({ $iconSpaceAfter, $passwordManagerPresent }) => ($iconSpaceAfter ? 45 : 16) + ($passwordManagerPresent ? pwWidth : 0)}px;
|
|
222
222
|
`;
|
|
223
223
|
|
|
224
|
-
export const ValidationText = styled.p<ValidationTextProps
|
|
224
|
+
export const ValidationText = styled.p.attrs<ValidationTextProps>(({ $showValidationMessage }) => ({
|
|
225
|
+
role: $showValidationMessage ? "alert" : undefined,
|
|
226
|
+
}))<ValidationTextProps>`
|
|
225
227
|
color: ${({ $disabled, $disabledColor, theme, $failColor: failColor }) =>
|
|
226
228
|
$disabled ? $disabledColor || theme.input?.disabledColor : failColor || theme.input?.failColor};
|
|
227
229
|
font-size: 14px;
|
package/src/sharedTypes/types.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface OptionType {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export interface OptionComponentLabelType extends Omit<OptionType, "label"> {
|
|
48
|
-
label?: string | JSX.Element;
|
|
48
|
+
label?: string | React.JSX.Element;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export interface ChildlessOptionType extends Omit<OptionType, "children" | "id" | "label"> {
|
package/tsconfig.json
CHANGED
package/tsconfig.test.json
CHANGED