@simplybusiness/mobius 5.3.1 → 5.4.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/dist/cjs/components/Checkbox/Checkbox.js +5 -20
- package/dist/cjs/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/cjs/components/Checkbox/CheckboxGroup.js +29 -11
- package/dist/cjs/components/Checkbox/CheckboxGroup.js.map +1 -1
- package/dist/cjs/hooks/index.js +1 -0
- package/dist/cjs/hooks/index.js.map +1 -1
- package/dist/cjs/hooks/useRenderCount/index.js +20 -0
- package/dist/cjs/hooks/useRenderCount/index.js.map +1 -0
- package/dist/cjs/hooks/useRenderCount/useRenderCount.js +20 -0
- package/dist/cjs/hooks/useRenderCount/useRenderCount.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/components/Checkbox/Checkbox.js +6 -21
- package/dist/esm/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/esm/components/Checkbox/CheckboxGroup.js +29 -11
- package/dist/esm/components/Checkbox/CheckboxGroup.js.map +1 -1
- package/dist/esm/components/Checkbox/types.js.map +1 -1
- package/dist/esm/hooks/index.js +1 -0
- package/dist/esm/hooks/index.js.map +1 -1
- package/dist/esm/hooks/useRenderCount/index.js +3 -0
- package/dist/esm/hooks/useRenderCount/index.js.map +1 -0
- package/dist/esm/hooks/useRenderCount/useRenderCount.js +10 -0
- package/dist/esm/hooks/useRenderCount/useRenderCount.js.map +1 -0
- package/dist/types/components/Checkbox/CheckboxGroup.stories.d.ts +1 -0
- package/dist/types/components/Checkbox/types.d.ts +10 -6
- package/dist/types/hooks/index.d.ts +1 -0
- package/dist/types/hooks/useRenderCount/index.d.ts +1 -0
- package/dist/types/hooks/useRenderCount/useRenderCount.d.ts +1 -0
- package/dist/types/hooks/useRenderCount/useRenderCount.test.d.ts +1 -0
- package/package.json +17 -17
- package/src/components/Checkbox/Checkbox.test.tsx +2 -1
- package/src/components/Checkbox/Checkbox.tsx +7 -31
- package/src/components/Checkbox/CheckboxGroup.stories.tsx +15 -0
- package/src/components/Checkbox/CheckboxGroup.test.tsx +107 -1
- package/src/components/Checkbox/CheckboxGroup.tsx +45 -15
- package/src/components/Checkbox/types.ts +13 -6
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useRenderCount/index.ts +1 -0
- package/src/hooks/useRenderCount/useRenderCount.test.ts +26 -0
- package/src/hooks/useRenderCount/useRenderCount.ts +9 -0
|
@@ -48,6 +48,18 @@ describe("CheckboxGroup", () => {
|
|
|
48
48
|
expect(errorMessage).toBeInTheDocument();
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
it("does not call onChange on initial render", () => {
|
|
52
|
+
const callback = jest.fn();
|
|
53
|
+
|
|
54
|
+
render(
|
|
55
|
+
<CheckboxGroup onChange={callback}>
|
|
56
|
+
<Checkbox label="Checkbox" value="value" />
|
|
57
|
+
</CheckboxGroup>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(callback).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
51
63
|
it("sets aria-describedby for all checkboxes when errorMessage is set", () => {
|
|
52
64
|
const errorMessageText = "Error message";
|
|
53
65
|
|
|
@@ -393,7 +405,7 @@ describe("CheckboxGroup", () => {
|
|
|
393
405
|
|
|
394
406
|
const { getByLabelText } = render(
|
|
395
407
|
<CheckboxGroup isInvalid errorMessage="There is a problem">
|
|
396
|
-
<Checkbox label={optionText} value="value"
|
|
408
|
+
<Checkbox label={optionText} value="value" />
|
|
397
409
|
</CheckboxGroup>,
|
|
398
410
|
);
|
|
399
411
|
|
|
@@ -503,4 +515,98 @@ describe("CheckboxGroup", () => {
|
|
|
503
515
|
expect(option).toHaveAttribute("readonly");
|
|
504
516
|
});
|
|
505
517
|
});
|
|
518
|
+
|
|
519
|
+
describe("last item disables", () => {
|
|
520
|
+
it("should disable all checkboxes except the last one when the last item is selected", async () => {
|
|
521
|
+
const option1Text = "Checkbox 1";
|
|
522
|
+
const option2Text = "Checkbox 2";
|
|
523
|
+
const option3Text = "Checkbox 3";
|
|
524
|
+
const option1Value = "value1";
|
|
525
|
+
const option2Value = "value2";
|
|
526
|
+
const option3Value = "value3";
|
|
527
|
+
|
|
528
|
+
render(
|
|
529
|
+
<CheckboxGroup lastItemDisables>
|
|
530
|
+
<Checkbox label={option1Text} value={option1Value} />
|
|
531
|
+
<Checkbox label={option2Text} value={option2Value} />
|
|
532
|
+
<Checkbox label={option3Text} value={option3Value} />
|
|
533
|
+
</CheckboxGroup>,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const option1 = screen.getByLabelText(option1Text);
|
|
537
|
+
const option2 = screen.getByLabelText(option2Text);
|
|
538
|
+
const option3 = screen.getByLabelText(option3Text);
|
|
539
|
+
|
|
540
|
+
await userEvent.click(option3);
|
|
541
|
+
|
|
542
|
+
expect(option1).toBeDisabled();
|
|
543
|
+
expect(option2).toBeDisabled();
|
|
544
|
+
expect(option3).not.toBeDisabled();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("should unchecked all checkboxes except the last one when the last item is selected", async () => {
|
|
548
|
+
const option1Text = "Checkbox 1";
|
|
549
|
+
const option2Text = "Checkbox 2";
|
|
550
|
+
const option3Text = "Checkbox 3";
|
|
551
|
+
const option1Value = "value1";
|
|
552
|
+
const option2Value = "value2";
|
|
553
|
+
const option3Value = "value3";
|
|
554
|
+
|
|
555
|
+
render(
|
|
556
|
+
<CheckboxGroup lastItemDisables>
|
|
557
|
+
<Checkbox label={option1Text} value={option1Value} />
|
|
558
|
+
<Checkbox label={option2Text} value={option2Value} />
|
|
559
|
+
<Checkbox label={option3Text} value={option3Value} />
|
|
560
|
+
</CheckboxGroup>,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const option1 = screen.getByLabelText(option1Text);
|
|
564
|
+
const option2 = screen.getByLabelText(option2Text);
|
|
565
|
+
const option3 = screen.getByLabelText(option3Text);
|
|
566
|
+
|
|
567
|
+
await userEvent.click(option1);
|
|
568
|
+
await userEvent.click(option3);
|
|
569
|
+
|
|
570
|
+
expect(option1).not.toBeChecked();
|
|
571
|
+
expect(option2).not.toBeChecked();
|
|
572
|
+
expect(option3).toBeChecked();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("should overwrite group value when last item is selected", async () => {
|
|
576
|
+
const callback = jest.fn();
|
|
577
|
+
|
|
578
|
+
const option1Text = "Checkbox 1";
|
|
579
|
+
const option2Text = "Checkbox 2";
|
|
580
|
+
const option3Text = "Checkbox 3";
|
|
581
|
+
const option1Value = "value1";
|
|
582
|
+
const option2Value = "value2";
|
|
583
|
+
const option3Value = "value3";
|
|
584
|
+
|
|
585
|
+
render(
|
|
586
|
+
<CheckboxGroup lastItemDisables onChange={callback}>
|
|
587
|
+
<Checkbox label={option1Text} value={option1Value} />
|
|
588
|
+
<Checkbox label={option2Text} value={option2Value} />
|
|
589
|
+
<Checkbox label={option3Text} value={option3Value} />
|
|
590
|
+
</CheckboxGroup>,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const option1 = screen.getByLabelText(option1Text);
|
|
594
|
+
const option2 = screen.getByLabelText(option2Text);
|
|
595
|
+
const option3 = screen.getByLabelText(option3Text);
|
|
596
|
+
|
|
597
|
+
await userEvent.click(option1);
|
|
598
|
+
await userEvent.click(option2);
|
|
599
|
+
|
|
600
|
+
expect(callback).toHaveBeenCalledWith([option1Value, option2Value]);
|
|
601
|
+
|
|
602
|
+
await userEvent.click(option3);
|
|
603
|
+
|
|
604
|
+
expect(callback).toHaveBeenCalledWith([option3Value]);
|
|
605
|
+
|
|
606
|
+
// Resets to empty array when last item unchecked again
|
|
607
|
+
await userEvent.click(option3);
|
|
608
|
+
|
|
609
|
+
expect(callback).toHaveBeenCalledWith([]);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
506
612
|
});
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import classNames from "classnames/dedupe";
|
|
3
4
|
import {
|
|
5
|
+
type ChangeEvent,
|
|
6
|
+
type ReactElement,
|
|
4
7
|
Children,
|
|
5
|
-
forwardRef,
|
|
6
|
-
ReactElement,
|
|
7
8
|
cloneElement,
|
|
9
|
+
forwardRef,
|
|
8
10
|
isValidElement,
|
|
11
|
+
useEffect,
|
|
9
12
|
useId,
|
|
10
|
-
ChangeEvent,
|
|
11
13
|
useState,
|
|
12
|
-
useEffect,
|
|
13
14
|
} from "react";
|
|
14
|
-
import
|
|
15
|
+
import { useRenderCount, useValidationClasses } from "../../hooks";
|
|
15
16
|
import { ForwardedRefComponent } from "../../types/components";
|
|
17
|
+
import { spaceDelimitedList } from "../../utils/spaceDelimitedList";
|
|
18
|
+
import { ErrorMessage } from "../ErrorMessage";
|
|
19
|
+
import { Label } from "../Label";
|
|
20
|
+
import { Checkbox } from "./Checkbox";
|
|
16
21
|
import {
|
|
17
22
|
CheckboxElementType,
|
|
18
23
|
CheckboxGroupElementType,
|
|
19
24
|
CheckboxGroupProps,
|
|
20
25
|
CheckboxGroupRef,
|
|
21
26
|
} from "./types";
|
|
22
|
-
import { Label } from "../Label";
|
|
23
|
-
import { ErrorMessage } from "../ErrorMessage";
|
|
24
|
-
import { spaceDelimitedList } from "../../utils/spaceDelimitedList";
|
|
25
|
-
import { useValidationClasses } from "../../hooks";
|
|
26
27
|
|
|
27
28
|
export const CheckboxGroup: ForwardedRefComponent<
|
|
28
29
|
CheckboxGroupProps,
|
|
@@ -42,6 +43,7 @@ export const CheckboxGroup: ForwardedRefComponent<
|
|
|
42
43
|
defaultValue = [],
|
|
43
44
|
isReadOnly,
|
|
44
45
|
itemsPerRow,
|
|
46
|
+
lastItemDisables = false,
|
|
45
47
|
...rest
|
|
46
48
|
} = props;
|
|
47
49
|
const [selected, setSelected] = useState<string[]>(defaultValue);
|
|
@@ -74,7 +76,10 @@ export const CheckboxGroup: ForwardedRefComponent<
|
|
|
74
76
|
]);
|
|
75
77
|
const labelId = useId();
|
|
76
78
|
|
|
77
|
-
const handleChange = (
|
|
79
|
+
const handleChange = (
|
|
80
|
+
event: ChangeEvent<CheckboxElementType>,
|
|
81
|
+
isLastItem = false,
|
|
82
|
+
) => {
|
|
78
83
|
const {
|
|
79
84
|
target: { value, checked },
|
|
80
85
|
} = event;
|
|
@@ -83,16 +88,34 @@ export const CheckboxGroup: ForwardedRefComponent<
|
|
|
83
88
|
setSelected(selected.filter(item => item !== value));
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
if (checked && lastItemDisables && isLastItem) {
|
|
92
|
+
setSelected([value]);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
if (checked) {
|
|
87
97
|
setSelected([...selected, value]);
|
|
88
98
|
}
|
|
89
99
|
};
|
|
90
100
|
|
|
101
|
+
// HACK: This is a workaround to ensure that the onChange event is not
|
|
102
|
+
// fired on the initial render.
|
|
103
|
+
const renderCount = useRenderCount();
|
|
91
104
|
useEffect(() => {
|
|
92
|
-
if (onChange) {
|
|
105
|
+
if (onChange && renderCount > 1) {
|
|
93
106
|
onChange(selected);
|
|
94
107
|
}
|
|
95
|
-
}, [selected, onChange]);
|
|
108
|
+
}, [selected, onChange, renderCount]);
|
|
109
|
+
|
|
110
|
+
const childrenArray = Children.toArray(children);
|
|
111
|
+
const lastCheckbox = childrenArray
|
|
112
|
+
.filter(
|
|
113
|
+
child =>
|
|
114
|
+
isValidElement(child) && (child as ReactElement).type === Checkbox,
|
|
115
|
+
)
|
|
116
|
+
.pop() as ReactElement<CheckboxElementType> | undefined;
|
|
117
|
+
const lastCheckboxIsChecked =
|
|
118
|
+
lastCheckbox && selected.includes(lastCheckbox.props.value);
|
|
96
119
|
|
|
97
120
|
return (
|
|
98
121
|
<div
|
|
@@ -113,14 +136,21 @@ export const CheckboxGroup: ForwardedRefComponent<
|
|
|
113
136
|
</Label>
|
|
114
137
|
)}
|
|
115
138
|
<div className="mobius-checkbox-group__wrapper">
|
|
116
|
-
{
|
|
139
|
+
{childrenArray.map(child => {
|
|
117
140
|
if (isValidElement(child)) {
|
|
141
|
+
// lastItemDisables support
|
|
142
|
+
const isLastItem = child === lastCheckbox;
|
|
143
|
+
const isChildDisabled =
|
|
144
|
+
isDisabled ||
|
|
145
|
+
(lastItemDisables && lastCheckboxIsChecked && !isLastItem);
|
|
146
|
+
|
|
118
147
|
return cloneElement(child as ReactElement, {
|
|
119
|
-
isDisabled,
|
|
148
|
+
isDisabled: isChildDisabled,
|
|
120
149
|
isRequired,
|
|
121
150
|
isReadOnly,
|
|
122
151
|
isInvalid,
|
|
123
|
-
|
|
152
|
+
isLastItem,
|
|
153
|
+
selected: selected.includes(child.props.value),
|
|
124
154
|
onChange: handleChange,
|
|
125
155
|
"aria-describedby": describedBy,
|
|
126
156
|
});
|
|
@@ -19,7 +19,10 @@ export interface CheckboxProps
|
|
|
19
19
|
value?: string;
|
|
20
20
|
// Whether the input is disabled.
|
|
21
21
|
isDisabled?: boolean;
|
|
22
|
-
onChange?: (
|
|
22
|
+
onChange?: (
|
|
23
|
+
event: ChangeEvent<CheckboxElementType>,
|
|
24
|
+
isLastItem?: boolean,
|
|
25
|
+
) => void;
|
|
23
26
|
// The default value (uncontrolled).
|
|
24
27
|
defaultSelected?: boolean;
|
|
25
28
|
// Whether the input can be selected but not changed by the user.
|
|
@@ -33,17 +36,17 @@ export interface CheckboxProps
|
|
|
33
36
|
*/
|
|
34
37
|
"aria-describedby"?: string;
|
|
35
38
|
/**
|
|
36
|
-
*
|
|
39
|
+
* Whether the checkbox is the last item of a group.
|
|
37
40
|
*/
|
|
38
|
-
|
|
41
|
+
isLastItem?: boolean;
|
|
39
42
|
/**
|
|
40
43
|
* **Internal:** Do not use
|
|
41
44
|
*/
|
|
42
|
-
|
|
45
|
+
groupDisabled?: boolean;
|
|
43
46
|
/**
|
|
44
|
-
*
|
|
47
|
+
* Whether the checkbox is selected.
|
|
45
48
|
*/
|
|
46
|
-
|
|
49
|
+
selected?: boolean;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export type CheckboxGroupElementType = HTMLDivElement;
|
|
@@ -81,6 +84,10 @@ interface CheckboxGroupPropsInternal
|
|
|
81
84
|
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio#Value).
|
|
82
85
|
*/
|
|
83
86
|
value?: string;
|
|
87
|
+
/**
|
|
88
|
+
* This determines if the last item in the group should disable the other items when selected.
|
|
89
|
+
*/
|
|
90
|
+
lastItemDisables?: boolean;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
interface HorizontalCheckboxGroupProps extends CheckboxGroupPropsInternal {
|
package/src/hooks/index.tsx
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./useDialogPolyfill";
|
|
|
7
7
|
export * from "./useLabel";
|
|
8
8
|
export * from "./useOnClickOutside";
|
|
9
9
|
export * from "./usePrefersReducedMotion";
|
|
10
|
+
export * from "./useRenderCount";
|
|
10
11
|
export * from "./useTextField";
|
|
11
12
|
export * from "./useValidationClasses";
|
|
12
13
|
export * from "./useWindowEvent";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useRenderCount";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { useRenderCount } from "./useRenderCount";
|
|
3
|
+
|
|
4
|
+
describe("useRenderCount", () => {
|
|
5
|
+
it("should return 1 on initial render", () => {
|
|
6
|
+
const { result } = renderHook(() => useRenderCount());
|
|
7
|
+
expect(result.current).toBe(1);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should increment the count on re-render", () => {
|
|
11
|
+
const { result, rerender } = renderHook(() => useRenderCount());
|
|
12
|
+
expect(result.current).toBe(1);
|
|
13
|
+
rerender();
|
|
14
|
+
expect(result.current).toBe(2);
|
|
15
|
+
rerender();
|
|
16
|
+
expect(result.current).toBe(3);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should maintain the count across multiple renders", () => {
|
|
20
|
+
const { result, rerender } = renderHook(() => useRenderCount());
|
|
21
|
+
rerender();
|
|
22
|
+
rerender();
|
|
23
|
+
rerender();
|
|
24
|
+
expect(result.current).toBe(4);
|
|
25
|
+
});
|
|
26
|
+
});
|