@jobber/components-native 0.35.0 → 0.36.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/dist/src/Checkbox/Checkbox.js +66 -0
- package/dist/src/Checkbox/Checkbox.style.js +30 -0
- package/dist/src/Checkbox/CheckboxGroup.js +115 -0
- package/dist/src/Checkbox/CheckboxGroup.style.js +14 -0
- package/dist/src/Checkbox/CheckboxGroupReducer.js +17 -0
- package/dist/src/Checkbox/index.js +2 -0
- package/dist/src/Checkbox/types.js +1 -0
- package/dist/src/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/Checkbox/Checkbox.d.ts +59 -0
- package/dist/types/src/Checkbox/Checkbox.style.d.ts +27 -0
- package/dist/types/src/Checkbox/CheckboxGroup.d.ts +22 -0
- package/dist/types/src/Checkbox/CheckboxGroup.style.d.ts +12 -0
- package/dist/types/src/Checkbox/CheckboxGroupReducer.d.ts +9 -0
- package/dist/types/src/Checkbox/index.d.ts +2 -0
- package/dist/types/src/Checkbox/types.d.ts +11 -0
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +3 -2
- package/src/Checkbox/Checkbox.style.ts +36 -0
- package/src/Checkbox/Checkbox.test.tsx +104 -0
- package/src/Checkbox/Checkbox.tsx +192 -0
- package/src/Checkbox/CheckboxGroup.style.ts +15 -0
- package/src/Checkbox/CheckboxGroup.test.tsx +341 -0
- package/src/Checkbox/CheckboxGroup.tsx +228 -0
- package/src/Checkbox/CheckboxGroupReducer.test.ts +41 -0
- package/src/Checkbox/CheckboxGroupReducer.ts +32 -0
- package/src/Checkbox/index.ts +2 -0
- package/src/Checkbox/types.ts +13 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
RenderAPI,
|
|
4
|
+
cleanup,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
waitFor,
|
|
8
|
+
} from "@testing-library/react-native";
|
|
9
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
10
|
+
import { CheckboxGroup } from "./CheckboxGroup";
|
|
11
|
+
import { Checkbox } from "./Checkbox";
|
|
12
|
+
import { CheckboxGroupState } from "./types";
|
|
13
|
+
import { Button } from "../Button";
|
|
14
|
+
|
|
15
|
+
const onSubmitMock = jest.fn();
|
|
16
|
+
|
|
17
|
+
const parentCheckboxLabel = "all condiments";
|
|
18
|
+
const firstCheckboxLabel = "relish";
|
|
19
|
+
const secondCheckboxLabel = "ketchup";
|
|
20
|
+
const thirdCheckboxLabel = "mustard";
|
|
21
|
+
const saveButtonText = "Save";
|
|
22
|
+
|
|
23
|
+
interface CheckboxGroupFormData {
|
|
24
|
+
[key: string]: CheckboxGroupState;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CheckboxGroupFormOnChangeHandlers {
|
|
28
|
+
relish?: (data: boolean) => void;
|
|
29
|
+
mustard?: (data: boolean) => void;
|
|
30
|
+
ketchup?: (data: boolean) => void;
|
|
31
|
+
all?: (data: CheckboxGroupState) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterEach(cleanup);
|
|
35
|
+
|
|
36
|
+
function setup(
|
|
37
|
+
label: string | undefined,
|
|
38
|
+
disabled?: boolean,
|
|
39
|
+
childDisabled?: boolean,
|
|
40
|
+
) {
|
|
41
|
+
const checkboxGroup = render(
|
|
42
|
+
<CheckboxGroup
|
|
43
|
+
name="condimentsGroupCheckbox"
|
|
44
|
+
label={label}
|
|
45
|
+
accessibilityLabel={parentCheckboxLabel}
|
|
46
|
+
disabled={disabled}
|
|
47
|
+
>
|
|
48
|
+
<Checkbox
|
|
49
|
+
label="Relish"
|
|
50
|
+
name="relish"
|
|
51
|
+
accessibilityLabel={firstCheckboxLabel}
|
|
52
|
+
/>
|
|
53
|
+
<Checkbox
|
|
54
|
+
label="Ketchup"
|
|
55
|
+
name="ketchup"
|
|
56
|
+
accessibilityLabel={secondCheckboxLabel}
|
|
57
|
+
disabled={childDisabled}
|
|
58
|
+
/>
|
|
59
|
+
<Checkbox
|
|
60
|
+
label="Mustard"
|
|
61
|
+
name="mustard"
|
|
62
|
+
accessibilityLabel={thirdCheckboxLabel}
|
|
63
|
+
/>
|
|
64
|
+
</CheckboxGroup>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return { checkboxGroup };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function SetupWithForm({
|
|
71
|
+
initialValues,
|
|
72
|
+
onChangeHandlers,
|
|
73
|
+
}: {
|
|
74
|
+
initialValues: CheckboxGroupFormData;
|
|
75
|
+
onChangeHandlers?: CheckboxGroupFormOnChangeHandlers;
|
|
76
|
+
}): JSX.Element {
|
|
77
|
+
const formMethods = useForm({ defaultValues: initialValues });
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<FormProvider {...formMethods}>
|
|
81
|
+
<CheckboxGroup
|
|
82
|
+
name="condiments"
|
|
83
|
+
label="All Condiments"
|
|
84
|
+
accessibilityLabel={parentCheckboxLabel}
|
|
85
|
+
onChange={onChangeHandlers?.all}
|
|
86
|
+
>
|
|
87
|
+
<Checkbox
|
|
88
|
+
label="Relish"
|
|
89
|
+
name="relish"
|
|
90
|
+
accessibilityLabel={firstCheckboxLabel}
|
|
91
|
+
onChange={onChangeHandlers?.relish}
|
|
92
|
+
/>
|
|
93
|
+
<Checkbox
|
|
94
|
+
label="Ketchup"
|
|
95
|
+
name="ketchup"
|
|
96
|
+
accessibilityLabel={secondCheckboxLabel}
|
|
97
|
+
onChange={onChangeHandlers?.ketchup}
|
|
98
|
+
/>
|
|
99
|
+
<Checkbox
|
|
100
|
+
label="Mustard"
|
|
101
|
+
name="mustard"
|
|
102
|
+
accessibilityLabel={thirdCheckboxLabel}
|
|
103
|
+
onChange={onChangeHandlers?.mustard}
|
|
104
|
+
/>
|
|
105
|
+
</CheckboxGroup>
|
|
106
|
+
<Button
|
|
107
|
+
onPress={formMethods.handleSubmit(values => onSubmitMock(values))}
|
|
108
|
+
label={saveButtonText}
|
|
109
|
+
accessibilityLabel={saveButtonText}
|
|
110
|
+
/>
|
|
111
|
+
</FormProvider>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe("when none of the checkboxes in a group are checked", () => {
|
|
116
|
+
it("the parent checkbox should be in the unchecked state", () => {
|
|
117
|
+
const { checkboxGroup } = setup("All Condiments");
|
|
118
|
+
const parentCheckbox = checkboxGroup.getByLabelText(parentCheckboxLabel);
|
|
119
|
+
expect(parentCheckbox.props.accessibilityState.checked).toEqual(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("when the parent checkbox is pressed", () => {
|
|
123
|
+
it("should check all the checkboxes within the group", () => {
|
|
124
|
+
const { checkboxGroup } = setup("All Condiments");
|
|
125
|
+
fireEvent.press(checkboxGroup.getByLabelText(parentCheckboxLabel));
|
|
126
|
+
[firstCheckboxLabel, secondCheckboxLabel, thirdCheckboxLabel].forEach(
|
|
127
|
+
checkboxLabel => {
|
|
128
|
+
const checkboxElement = checkboxGroup.getByLabelText(checkboxLabel);
|
|
129
|
+
expect(checkboxElement.props.accessibilityState.checked).toEqual(
|
|
130
|
+
true,
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("when one of the checkboxes in a group are checked", () => {
|
|
139
|
+
it("the parent checkbox should be in the indeterminate state", () => {
|
|
140
|
+
const { checkboxGroup } = setup("All Condiments");
|
|
141
|
+
fireEvent.press(checkboxGroup.getByLabelText(secondCheckboxLabel));
|
|
142
|
+
const parentCheckbox = checkboxGroup.getByLabelText(parentCheckboxLabel);
|
|
143
|
+
expect(parentCheckbox.props.accessibilityState.checked).toEqual("mixed");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("when all of the checkboxes in a group are checked", () => {
|
|
148
|
+
function pressAllCheckboxes(checkboxGroup: RenderAPI) {
|
|
149
|
+
fireEvent.press(checkboxGroup.getByLabelText(firstCheckboxLabel));
|
|
150
|
+
fireEvent.press(checkboxGroup.getByLabelText(secondCheckboxLabel));
|
|
151
|
+
fireEvent.press(checkboxGroup.getByLabelText(thirdCheckboxLabel));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
it("the parent checkbox should be in the checked state", () => {
|
|
155
|
+
const { checkboxGroup } = setup("All Condiments");
|
|
156
|
+
pressAllCheckboxes(checkboxGroup);
|
|
157
|
+
const parentCheckbox = checkboxGroup.getByLabelText(parentCheckboxLabel);
|
|
158
|
+
expect(parentCheckbox.props.accessibilityState.checked).toEqual(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("when the parent checkbox is pressed", () => {
|
|
162
|
+
it("should uncheck all the checkboxes within the group", () => {
|
|
163
|
+
const { checkboxGroup } = setup("All Condiments");
|
|
164
|
+
pressAllCheckboxes(checkboxGroup);
|
|
165
|
+
fireEvent.press(checkboxGroup.getByLabelText(parentCheckboxLabel));
|
|
166
|
+
[firstCheckboxLabel, secondCheckboxLabel, thirdCheckboxLabel].forEach(
|
|
167
|
+
checkboxLabel => {
|
|
168
|
+
const checkboxElement = checkboxGroup.getByLabelText(checkboxLabel);
|
|
169
|
+
expect(checkboxElement.props.accessibilityState.checked).toEqual(
|
|
170
|
+
false,
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("when the parent checkbox does not have a label", () => {
|
|
179
|
+
it("does not render the parent checkbox", async () => {
|
|
180
|
+
const { checkboxGroup } = setup(undefined);
|
|
181
|
+
const findParentCheckbox = () => {
|
|
182
|
+
checkboxGroup.getByLabelText(parentCheckboxLabel);
|
|
183
|
+
};
|
|
184
|
+
expect(findParentCheckbox).toThrow(
|
|
185
|
+
"Unable to find an element with accessibilityLabel: all condiments",
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("when the parent checkbox is disabled", () => {
|
|
191
|
+
it("disabled all the children checkboxes", () => {
|
|
192
|
+
const { checkboxGroup } = setup("All Condiments", true);
|
|
193
|
+
fireEvent.press(checkboxGroup.getByLabelText(parentCheckboxLabel));
|
|
194
|
+
[firstCheckboxLabel, secondCheckboxLabel, thirdCheckboxLabel].forEach(
|
|
195
|
+
checkboxLabel => {
|
|
196
|
+
const checkboxElement = checkboxGroup.getByLabelText(checkboxLabel);
|
|
197
|
+
expect(checkboxElement.props.accessibilityState.disabled).toEqual(true);
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("when one of the checkboxes in a group are disabled", () => {
|
|
204
|
+
it("the parent shouldn't modify the disabled checkbox's state", () => {
|
|
205
|
+
const parentDisabled = false;
|
|
206
|
+
const childDisabled = true;
|
|
207
|
+
const { checkboxGroup } = setup(
|
|
208
|
+
"All Condiments",
|
|
209
|
+
parentDisabled,
|
|
210
|
+
childDisabled,
|
|
211
|
+
);
|
|
212
|
+
fireEvent.press(checkboxGroup.getByLabelText(parentCheckboxLabel));
|
|
213
|
+
|
|
214
|
+
[firstCheckboxLabel, thirdCheckboxLabel].forEach(checkboxLabel => {
|
|
215
|
+
const checkboxElement = checkboxGroup.getByLabelText(checkboxLabel);
|
|
216
|
+
expect(checkboxElement.props.accessibilityState.checked).toEqual(true);
|
|
217
|
+
});
|
|
218
|
+
const parentCheckbox = checkboxGroup.getByLabelText(parentCheckboxLabel);
|
|
219
|
+
expect(parentCheckbox.props.accessibilityState.checked).toEqual("mixed");
|
|
220
|
+
});
|
|
221
|
+
it("the parent should update the non disabled checkboxes", () => {
|
|
222
|
+
const parentDisabled = false;
|
|
223
|
+
const childDisabled = true;
|
|
224
|
+
const { checkboxGroup } = setup(
|
|
225
|
+
"All Condiments",
|
|
226
|
+
parentDisabled,
|
|
227
|
+
childDisabled,
|
|
228
|
+
);
|
|
229
|
+
fireEvent.press(checkboxGroup.getByLabelText(parentCheckboxLabel));
|
|
230
|
+
|
|
231
|
+
const secondCheckbox = checkboxGroup.getByLabelText(secondCheckboxLabel);
|
|
232
|
+
const parentCheckbox = checkboxGroup.getByLabelText(parentCheckboxLabel);
|
|
233
|
+
expect(secondCheckbox.props.accessibilityState.checked).toEqual(false);
|
|
234
|
+
expect(parentCheckbox.props.accessibilityState.checked).toEqual("mixed");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("when one of the checkbox has no name", () => {
|
|
239
|
+
it("should throw an error", () => {
|
|
240
|
+
expect(() =>
|
|
241
|
+
render(
|
|
242
|
+
<CheckboxGroup
|
|
243
|
+
name="condimentsGroupCheckbox"
|
|
244
|
+
label="test"
|
|
245
|
+
accessibilityLabel={parentCheckboxLabel}
|
|
246
|
+
>
|
|
247
|
+
<Checkbox
|
|
248
|
+
label="Relish"
|
|
249
|
+
name="relish"
|
|
250
|
+
accessibilityLabel={firstCheckboxLabel}
|
|
251
|
+
/>
|
|
252
|
+
<Checkbox
|
|
253
|
+
label="Ketchup"
|
|
254
|
+
name="ketchup"
|
|
255
|
+
accessibilityLabel={secondCheckboxLabel}
|
|
256
|
+
/>
|
|
257
|
+
<Checkbox onChange={jest.fn} checked={false} />
|
|
258
|
+
</CheckboxGroup>,
|
|
259
|
+
),
|
|
260
|
+
).toThrow("You must provide a name to checkboxes in a checkbox group");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("when controlled by a form", () => {
|
|
265
|
+
describe("when a checkbox is pressed", () => {
|
|
266
|
+
it("should update the form values", async () => {
|
|
267
|
+
const initialValues = {
|
|
268
|
+
condiments: {
|
|
269
|
+
parentChecked: false,
|
|
270
|
+
childrenChecked: { relish: false, ketchup: false, mustard: false },
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const expectedValues = {
|
|
274
|
+
condiments: {
|
|
275
|
+
parentChecked: false,
|
|
276
|
+
childrenChecked: { relish: false, ketchup: true, mustard: false },
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
const { getByLabelText } = render(
|
|
280
|
+
<SetupWithForm initialValues={initialValues} />,
|
|
281
|
+
);
|
|
282
|
+
fireEvent.press(getByLabelText(secondCheckboxLabel));
|
|
283
|
+
const saveButton = getByLabelText(saveButtonText);
|
|
284
|
+
await waitFor(() => {
|
|
285
|
+
fireEvent.press(saveButton);
|
|
286
|
+
});
|
|
287
|
+
expect(onSubmitMock).toHaveBeenCalledWith(expectedValues);
|
|
288
|
+
});
|
|
289
|
+
it("should call the onChange handler for child checkbox", async () => {
|
|
290
|
+
const initialValues = {
|
|
291
|
+
condiments: {
|
|
292
|
+
parentChecked: false,
|
|
293
|
+
childrenChecked: { relish: false, ketchup: false, mustard: false },
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
const relishHandler = jest.fn();
|
|
297
|
+
const onChangeHandlers: CheckboxGroupFormOnChangeHandlers = {
|
|
298
|
+
relish: relishHandler,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const { getByLabelText } = render(
|
|
302
|
+
<SetupWithForm
|
|
303
|
+
initialValues={initialValues}
|
|
304
|
+
onChangeHandlers={onChangeHandlers}
|
|
305
|
+
/>,
|
|
306
|
+
);
|
|
307
|
+
fireEvent.press(getByLabelText(firstCheckboxLabel));
|
|
308
|
+
|
|
309
|
+
expect(relishHandler).toHaveBeenCalledWith(true);
|
|
310
|
+
});
|
|
311
|
+
it("should call the onChange handler for parent checkbox", async () => {
|
|
312
|
+
const initialValues = {
|
|
313
|
+
condiments: {
|
|
314
|
+
parentChecked: false,
|
|
315
|
+
childrenChecked: { relish: false, ketchup: false, mustard: false },
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
const allHandler = jest.fn();
|
|
319
|
+
const onChangeHandlers: CheckboxGroupFormOnChangeHandlers = {
|
|
320
|
+
all: allHandler,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const { getByLabelText } = render(
|
|
324
|
+
<SetupWithForm
|
|
325
|
+
initialValues={initialValues}
|
|
326
|
+
onChangeHandlers={onChangeHandlers}
|
|
327
|
+
/>,
|
|
328
|
+
);
|
|
329
|
+
fireEvent.press(getByLabelText(parentCheckboxLabel));
|
|
330
|
+
|
|
331
|
+
expect(allHandler).toHaveBeenCalledWith({
|
|
332
|
+
parentChecked: true,
|
|
333
|
+
childrenChecked: {
|
|
334
|
+
relish: true,
|
|
335
|
+
ketchup: true,
|
|
336
|
+
mustard: true,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import React, { Fragment, useReducer } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import isEmpty from "lodash/isEmpty";
|
|
4
|
+
import reduce from "lodash/reduce";
|
|
5
|
+
import { XOR } from "ts-xor";
|
|
6
|
+
import { styles } from "./CheckboxGroup.style";
|
|
7
|
+
import { Checkbox, CheckboxProps } from "./Checkbox";
|
|
8
|
+
import { CheckboxElement, CheckboxGroupState } from "./types";
|
|
9
|
+
import {
|
|
10
|
+
checkboxGroupReducer,
|
|
11
|
+
initCheckboxGroupState,
|
|
12
|
+
} from "./CheckboxGroupReducer";
|
|
13
|
+
import { FormField } from "../FormField";
|
|
14
|
+
import { Divider } from "../Divider";
|
|
15
|
+
|
|
16
|
+
interface CommonCheckboxGroupProps extends Omit<CheckboxProps, "onChange"> {
|
|
17
|
+
/**
|
|
18
|
+
* Checkbox items
|
|
19
|
+
*/
|
|
20
|
+
children: CheckboxElement[];
|
|
21
|
+
|
|
22
|
+
state?: CheckboxGroupState;
|
|
23
|
+
|
|
24
|
+
onChange?(groupChecks: CheckboxGroupState): void;
|
|
25
|
+
}
|
|
26
|
+
interface ControlledCheckboxGroupProps extends CommonCheckboxGroupProps {
|
|
27
|
+
state: CheckboxGroupState;
|
|
28
|
+
onChange(groupChecks: CheckboxGroupState): void;
|
|
29
|
+
}
|
|
30
|
+
interface UncontrolledCheckboxGroupProps extends CommonCheckboxGroupProps {
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
interface ChildCheckboxObject {
|
|
34
|
+
[key: string]: React.ReactElement<CheckboxProps>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type CheckboxGroupProps = XOR<
|
|
38
|
+
UncontrolledCheckboxGroupProps,
|
|
39
|
+
ControlledCheckboxGroupProps
|
|
40
|
+
>;
|
|
41
|
+
export function CheckboxGroup({
|
|
42
|
+
children,
|
|
43
|
+
state,
|
|
44
|
+
onChange,
|
|
45
|
+
name,
|
|
46
|
+
...rest
|
|
47
|
+
}: CheckboxGroupProps): JSX.Element {
|
|
48
|
+
if (state !== undefined && onChange !== undefined) {
|
|
49
|
+
return (
|
|
50
|
+
<CheckboxGroupInternal
|
|
51
|
+
state={state}
|
|
52
|
+
onChange={onChange}
|
|
53
|
+
name={name}
|
|
54
|
+
{...rest}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</CheckboxGroupInternal>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (name) {
|
|
61
|
+
return (
|
|
62
|
+
<FormField name={name}>
|
|
63
|
+
{field => {
|
|
64
|
+
return (
|
|
65
|
+
<CheckboxGroupInternal
|
|
66
|
+
name={field.name}
|
|
67
|
+
state={field.value}
|
|
68
|
+
onChange={newValue => {
|
|
69
|
+
onChange?.(newValue);
|
|
70
|
+
field.onChange(newValue);
|
|
71
|
+
}}
|
|
72
|
+
{...rest}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</CheckboxGroupInternal>
|
|
76
|
+
);
|
|
77
|
+
}}
|
|
78
|
+
</FormField>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
throw new Error("CheckboxGroup passed invalid props");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function CheckboxGroupInternal({
|
|
85
|
+
label,
|
|
86
|
+
disabled,
|
|
87
|
+
children,
|
|
88
|
+
state,
|
|
89
|
+
accessibilityLabel,
|
|
90
|
+
onChange,
|
|
91
|
+
name: parentName,
|
|
92
|
+
}: ControlledCheckboxGroupProps): JSX.Element {
|
|
93
|
+
const childrenNames = React.Children.map(children, child => {
|
|
94
|
+
const name = throwErrorIfItHasNoName(child.props.name);
|
|
95
|
+
|
|
96
|
+
return name;
|
|
97
|
+
});
|
|
98
|
+
const isNested = !!label;
|
|
99
|
+
const [internalCheckedValues, dispatch] = useReducer(
|
|
100
|
+
checkboxGroupReducer,
|
|
101
|
+
childrenNames,
|
|
102
|
+
initCheckboxGroupState,
|
|
103
|
+
);
|
|
104
|
+
const actualCheckedValues = !isEmpty(state) ? state : internalCheckedValues;
|
|
105
|
+
|
|
106
|
+
const handleChange = (data: CheckboxGroupState) => {
|
|
107
|
+
dispatch({ type: "Update", data });
|
|
108
|
+
onChange?.(data);
|
|
109
|
+
};
|
|
110
|
+
const indeterminate = checkIndeterminateStatus(actualCheckedValues);
|
|
111
|
+
|
|
112
|
+
function cloneChildCheckbox(
|
|
113
|
+
checkbox: React.ReactElement<CheckboxProps>,
|
|
114
|
+
): React.ReactElement<CheckboxProps> {
|
|
115
|
+
const name = throwErrorIfItHasNoName(checkbox.props.name);
|
|
116
|
+
const childDisabled = disabled || checkbox.props.disabled;
|
|
117
|
+
const childrenHandleChange = (checked: boolean) => {
|
|
118
|
+
const childrenNextValue = {
|
|
119
|
+
...actualCheckedValues.childrenChecked,
|
|
120
|
+
[name]: checked,
|
|
121
|
+
};
|
|
122
|
+
const parentNextValue = reduce(childrenNextValue, getParentChecked, true);
|
|
123
|
+
checkbox.props.onChange?.(checked);
|
|
124
|
+
handleChange({
|
|
125
|
+
childrenChecked: childrenNextValue,
|
|
126
|
+
parentChecked: parentNextValue,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
return React.cloneElement(checkbox, {
|
|
130
|
+
onChange: childrenHandleChange,
|
|
131
|
+
checked: actualCheckedValues.childrenChecked[name],
|
|
132
|
+
disabled: childDisabled,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const checkboxObject: ChildCheckboxObject = React.Children.toArray(
|
|
137
|
+
children,
|
|
138
|
+
).reduce((childCheckboxObject: ChildCheckboxObject, child) => {
|
|
139
|
+
if (!React.isValidElement<CheckboxProps>(child)) {
|
|
140
|
+
return childCheckboxObject;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const name = throwErrorIfItHasNoName(child.props.name);
|
|
144
|
+
return {
|
|
145
|
+
...childCheckboxObject,
|
|
146
|
+
[name]: cloneChildCheckbox(child),
|
|
147
|
+
};
|
|
148
|
+
}, {});
|
|
149
|
+
|
|
150
|
+
function getParentChecked(
|
|
151
|
+
acc: boolean,
|
|
152
|
+
value: boolean,
|
|
153
|
+
childName: string,
|
|
154
|
+
): boolean {
|
|
155
|
+
const currentCheckbox = checkboxObject[childName];
|
|
156
|
+
if (currentCheckbox?.props?.disabled) {
|
|
157
|
+
return acc;
|
|
158
|
+
}
|
|
159
|
+
return acc && value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<View>
|
|
164
|
+
{isNested ? (
|
|
165
|
+
<>
|
|
166
|
+
<Checkbox
|
|
167
|
+
name={parentName}
|
|
168
|
+
label={label}
|
|
169
|
+
accessibilityLabel={accessibilityLabel || label}
|
|
170
|
+
indeterminate={indeterminate}
|
|
171
|
+
checked={actualCheckedValues.parentChecked || false}
|
|
172
|
+
onChange={value => {
|
|
173
|
+
const newValues = reduce(
|
|
174
|
+
actualCheckedValues.childrenChecked,
|
|
175
|
+
(acc, currentCheckboxValue, childName) => {
|
|
176
|
+
const currentCheckbox = checkboxObject[childName];
|
|
177
|
+
return currentCheckbox?.props?.disabled
|
|
178
|
+
? {
|
|
179
|
+
...acc,
|
|
180
|
+
[childName]: currentCheckboxValue,
|
|
181
|
+
}
|
|
182
|
+
: {
|
|
183
|
+
...acc,
|
|
184
|
+
[childName]: value,
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
{},
|
|
188
|
+
);
|
|
189
|
+
const parentChecked = reduce(newValues, getParentChecked, value);
|
|
190
|
+
onChange({ childrenChecked: newValues, parentChecked });
|
|
191
|
+
}}
|
|
192
|
+
disabled={disabled}
|
|
193
|
+
/>
|
|
194
|
+
<Divider />
|
|
195
|
+
</>
|
|
196
|
+
) : undefined}
|
|
197
|
+
<View />
|
|
198
|
+
<View style={isNested ? styles.nestedCheckboxes : {}}>
|
|
199
|
+
{Object.values(checkboxObject).map((checkbox, index) => {
|
|
200
|
+
return (
|
|
201
|
+
<Fragment key={index}>
|
|
202
|
+
{checkbox}
|
|
203
|
+
{index !== children.length - 1 && <Divider />}
|
|
204
|
+
</Fragment>
|
|
205
|
+
);
|
|
206
|
+
})}
|
|
207
|
+
</View>
|
|
208
|
+
</View>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function throwErrorIfItHasNoName(name?: string): string {
|
|
213
|
+
if (!name) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"You must provide a name to checkboxes in a checkbox group",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return name;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function checkIndeterminateStatus(checkedValues: CheckboxGroupState): boolean {
|
|
222
|
+
const checkedValuesAsArray = Object.values(checkedValues.childrenChecked);
|
|
223
|
+
if (checkedValuesAsArray.length === 1) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return !checkedValuesAsArray.every((value, i, arr) => value === arr[0]);
|
|
228
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkboxGroupReducer,
|
|
3
|
+
initCheckboxGroupState,
|
|
4
|
+
} from "./CheckboxGroupReducer";
|
|
5
|
+
import { CheckboxGroupState } from "./types";
|
|
6
|
+
|
|
7
|
+
let state: CheckboxGroupState;
|
|
8
|
+
const checkbox1 = "checkbox1";
|
|
9
|
+
const checkbox2 = "checkbox2";
|
|
10
|
+
const checkbox3 = "checkbox3";
|
|
11
|
+
const childrenNames = [checkbox1, checkbox2, checkbox3];
|
|
12
|
+
|
|
13
|
+
describe("update", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
state = initCheckboxGroupState(childrenNames);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should update childrenChecked", () => {
|
|
19
|
+
const result = checkboxGroupReducer(state, {
|
|
20
|
+
type: "Update",
|
|
21
|
+
data: { childrenChecked: { [checkbox1]: true } },
|
|
22
|
+
});
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
...state,
|
|
25
|
+
childrenChecked: {
|
|
26
|
+
...state.childrenChecked,
|
|
27
|
+
[checkbox1]: true,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it("should update parentChecked", () => {
|
|
32
|
+
const result = checkboxGroupReducer(state, {
|
|
33
|
+
type: "Update",
|
|
34
|
+
data: { parentChecked: true },
|
|
35
|
+
});
|
|
36
|
+
expect(result).toEqual({
|
|
37
|
+
...state,
|
|
38
|
+
parentChecked: true,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import deepmerge from "deepmerge";
|
|
2
|
+
import { CheckboxGroupState, CheckboxGroupStateInitializer } from "./types";
|
|
3
|
+
|
|
4
|
+
interface UpdateAction {
|
|
5
|
+
type: "Update";
|
|
6
|
+
data: Partial<CheckboxGroupState>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type CheckboxGroupAction = UpdateAction;
|
|
10
|
+
|
|
11
|
+
export function initCheckboxGroupState(
|
|
12
|
+
childNames: CheckboxGroupStateInitializer,
|
|
13
|
+
): CheckboxGroupState {
|
|
14
|
+
return {
|
|
15
|
+
parentChecked: false,
|
|
16
|
+
childrenChecked: childNames.reduce((acc, name) => {
|
|
17
|
+
return { ...acc, [name]: false };
|
|
18
|
+
}, {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function checkboxGroupReducer(
|
|
23
|
+
state: CheckboxGroupState,
|
|
24
|
+
action: CheckboxGroupAction,
|
|
25
|
+
): CheckboxGroupState {
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
case "Update":
|
|
28
|
+
return deepmerge(state, action.data);
|
|
29
|
+
default:
|
|
30
|
+
return state;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
import { Checkbox, CheckboxProps } from "./Checkbox";
|
|
3
|
+
|
|
4
|
+
export interface CheckboxGroupChildrenState {
|
|
5
|
+
[key: string]: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CheckboxGroupState {
|
|
9
|
+
parentChecked?: boolean;
|
|
10
|
+
childrenChecked: CheckboxGroupChildrenState;
|
|
11
|
+
}
|
|
12
|
+
export type CheckboxGroupStateInitializer = string[];
|
|
13
|
+
export type CheckboxElement = ReactElement<CheckboxProps, typeof Checkbox>;
|