diff --git a/src/components/CheckboxGroup.js b/src/components/CheckboxGroup.js
new file mode 100644
index 00000000..e9835c02
--- /dev/null
+++ b/src/components/CheckboxGroup.js
@@ -0,0 +1,172 @@
+import React, { useState, useMemo, useCallback } from "react";
+import PropTypes from "prop-types";
+import { nanoid } from "nanoid";
+import useField from "../hooks/internal/useField";
+import InternalCheckbox from "./internal/InternalCheckbox";
+import Field from "./internal/Field";
+import Stack from "./Stack";
+import { mergeProps, areCheckboxOptionsValid } from "../utils/component";
+
+const { COLORS } = InternalCheckbox;
+
+const DEFAULT_PROPS = {
+ color: InternalCheckbox.DEFAULT_PROPS.color,
+ disabled: false,
+ optional: false,
+ validate: (value, { isEmpty }) => {
+ if (isEmpty(value)) {
+ return "Please make a selection.";
+ }
+
+ return null;
+ },
+};
+
+CheckboxGroup.COLORS = COLORS;
+CheckboxGroup.DEFAULT_PROPS = DEFAULT_PROPS;
+
+function getKeyFromName(name) {
+ const index = name.lastIndexOf(".");
+
+ return name.slice(index + 1);
+}
+
+function CheckboxGroup(props) {
+ const mergedProps = mergeProps(
+ props,
+ DEFAULT_PROPS,
+ {},
+ {
+ color: (color) => COLORS.includes(color),
+ disabled: (disabled) => typeof disabled === "boolean",
+ optional: (optional) => typeof optional === "boolean",
+ options: (options) => areCheckboxOptionsValid(options),
+ }
+ );
+ const {
+ name,
+ label,
+ options,
+ helpText,
+ disabled,
+ optional,
+ validate,
+ validateData,
+ onChange: onChangeProp,
+ testId,
+ } = mergedProps;
+
+ if (!options) {
+ throw new Error("CheckboxGroup options are invalid");
+ }
+
+ const [labelId] = useState(() => `checkbox-group-label-${nanoid()}`);
+ const [auxId] = useState(() => `checkbox-group-aux-${nanoid()}`);
+ const isEmpty = useCallback((value) => {
+ for (const key in value) {
+ if (value[key] === true) {
+ return false;
+ }
+ }
+
+ return true;
+ }, []);
+ const data = useMemo(
+ () => ({
+ isEmpty,
+ ...(validateData && { data: validateData }),
+ }),
+ [isEmpty, validateData]
+ );
+ const {
+ value,
+ errors,
+ hasErrors,
+ onFocus,
+ onBlur,
+ onChange: fieldOnChange,
+ onMouseDown,
+ } = useField("CheckboxGroup", {
+ name,
+ disabled,
+ optional,
+ validate,
+ data,
+ });
+ const onChange = useCallback(
+ (event) => {
+ fieldOnChange(event);
+
+ onChangeProp &&
+ onChangeProp({
+ value: {
+ ...value,
+ [getKeyFromName(event.target.name)]: event.target.checked,
+ },
+ });
+ },
+ [fieldOnChange, onChangeProp, value]
+ );
+
+ return (
+
+
+ {options.map(({ key, label }) => (
+
+ {label}
+
+ ))}
+
+
+ );
+}
+
+CheckboxGroup.propTypes = {
+ name: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ options: PropTypes.oneOfType([
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ })
+ ),
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ label: PropTypes.node.isRequired,
+ })
+ ),
+ ]).isRequired,
+ color: PropTypes.oneOf(COLORS),
+ helpText: PropTypes.string,
+ disabled: PropTypes.bool,
+ optional: PropTypes.bool,
+ validate: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
+ validateData: PropTypes.any,
+ onChange: PropTypes.func,
+ testId: PropTypes.string,
+};
+
+export default CheckboxGroup;
diff --git a/src/components/Form.js b/src/components/Form.js
index 974e541d..95461ced 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -104,7 +104,10 @@ function Form(_props) {
2. Press the Checkbox without releasing it (validation error appears).
3. If you resease the Checkbox now, the validation error disappears.
*/
- if (state.shouldValidateOnChange || isCheckbox) {
+ if (
+ state.shouldValidateOnChange ||
+ (isCheckbox && target.dataset.parentName === undefined)
+ ) {
newState = setPath(newState, "namesToValidate", [
getParentFieldName(target),
]);
diff --git a/src/components/index.js b/src/components/index.js
index 222b2d22..daf05b0d 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -1,6 +1,7 @@
export { default as Accordion } from "./Accordion";
export { default as Button } from "./Button";
export { default as Checkbox } from "./Checkbox";
+export { default as CheckboxGroup } from "./CheckboxGroup";
export { default as Container } from "./Container";
export { default as DatePicker } from "./DatePicker";
export { default as Divider } from "./Divider";
diff --git a/src/components/internal/InternalCheckbox.js b/src/components/internal/InternalCheckbox.js
index 512ae6d1..81d8f780 100644
--- a/src/components/internal/InternalCheckbox.js
+++ b/src/components/internal/InternalCheckbox.js
@@ -1,5 +1,6 @@
-import React from "react";
+import React, { useState } from "react";
import PropTypes from "prop-types";
+import { nanoid } from "nanoid";
import useTheme from "../../hooks/useTheme";
import useBackground from "../../hooks/useBackground";
import useResponsivePropsCSS from "../../hooks/useResponsivePropsCSS";
@@ -65,7 +66,6 @@ function InternalCheckbox(_props) {
const {
name,
parentName,
- inputId,
color,
disabled,
isValid,
@@ -81,6 +81,7 @@ function InternalCheckbox(_props) {
} = props;
const theme = useTheme();
const { inputColorMap } = useBackground();
+ const [inputId] = useState(() => props.inputId ?? `checkbox-${nanoid()}`);
const labelCSS = useResponsivePropsCSS(props, DEFAULT_PROPS, {
color: (propsAtBreakpoint, theme, bp) => {
return theme.checkbox.getCSS({
@@ -127,7 +128,7 @@ function InternalCheckbox(_props) {
InternalCheckbox.propTypes = {
name: PropTypes.string.isRequired,
parentName: PropTypes.string,
- inputId: PropTypes.string.isRequired,
+ inputId: PropTypes.string,
color: PropTypes.oneOf(COLORS),
disabled: PropTypes.bool,
isValid: PropTypes.bool.isRequired,
diff --git a/src/utils/component.js b/src/utils/component.js
index 7ef4633a..52ee859b 100644
--- a/src/utils/component.js
+++ b/src/utils/component.js
@@ -48,6 +48,26 @@ export function areOptionsValid(options) {
return true;
}
+export function areCheckboxOptionsValid(options) {
+ if (!Array.isArray(options) || options.length === 0) {
+ return false;
+ }
+
+ for (const option of options) {
+ if (
+ !(
+ typeof option.key === "string" &&
+ ((typeof option.label === "string" && option.label.trim() !== "") ||
+ React.isValidElement(option.label))
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
export function areDropdownOptionsValid(options) {
if (!Array.isArray(options) || options.length === 0) {
return false;
diff --git a/website/gatsby-config.js b/website/gatsby-config.js
index 7353dae6..9cb16b57 100644
--- a/website/gatsby-config.js
+++ b/website/gatsby-config.js
@@ -14,6 +14,9 @@ module.exports = {
Checkbox: {
status: COMPONENT_STATUS.READY,
},
+ CheckboxGroup: {
+ status: COMPONENT_STATUS.READY,
+ },
Container: {
status: COMPONENT_STATUS.READY,
},
diff --git a/website/src/components/Sidebar.js b/website/src/components/Sidebar.js
index 45e48dcd..ca152485 100644
--- a/website/src/components/Sidebar.js
+++ b/website/src/components/Sidebar.js
@@ -72,7 +72,6 @@ function Sidebar() {
query ComponentsQuery {
allFile(
filter: { relativePath: { regex: "/^components/.*/index.js/" } }
- sort: { order: ASC, fields: relativePath }
) {
edges {
node {
@@ -82,14 +81,18 @@ function Sidebar() {
}
}
`);
- const components = data.allFile.edges.map(({ node }) => {
- const { relativeDirectory } = node;
+ const components = data.allFile.edges
+ .map(({ node }) => {
+ const { relativeDirectory } = node;
- return {
- componentName: pascalCase(relativeDirectory.split("/")[1]),
- href: `/${relativeDirectory}`,
- };
- });
+ return {
+ componentName: pascalCase(relativeDirectory.split("/")[1]),
+ href: `/${relativeDirectory}`,
+ };
+ })
+ .sort(({ componentName: name1 }, { componentName: name2 }) =>
+ name1 < name2 ? -1 : 1
+ );
return (
diff --git a/website/src/pages/components/checkbox-group/index.js b/website/src/pages/components/checkbox-group/index.js
new file mode 100644
index 00000000..ef5d94c7
--- /dev/null
+++ b/website/src/pages/components/checkbox-group/index.js
@@ -0,0 +1,148 @@
+import React, { useState } from "react";
+import * as allDesignSystem from "basis";
+import ComponentContainer from "../../../components/ComponentContainer";
+import RadioGroupSetting, {
+ getRadioOptions,
+ getCheckboxOptions,
+} from "../../../components/RadioGroupSetting";
+import { formatCode, nonDefaultProps } from "../../../utils/formatting";
+
+const { useTheme, CheckboxGroup } = allDesignSystem;
+const { COLORS, DEFAULT_PROPS } = CheckboxGroup;
+const scope = allDesignSystem;
+
+const colorOptions = getRadioOptions(COLORS);
+const isOptionalOptions = getCheckboxOptions();
+const hasHelpTextOptions = getCheckboxOptions();
+const isDisabledOptions = getCheckboxOptions();
+
+function CheckboxGroupPage() {
+ const theme = useTheme();
+ const [color, setColor] = useState(DEFAULT_PROPS.color);
+ const [optional, setIsOptional] = useState(DEFAULT_PROPS.optional);
+ const [hasHelpText, setHasHelpText] = useState(
+ Boolean(DEFAULT_PROPS.helpText)
+ );
+ const [disabled, setIsDisabled] = useState(DEFAULT_PROPS.disabled);
+ const code = formatCode(`
+ const options = [
+ {
+ key: 'apple',
+ label: 'Apple'
+ },
+ {
+ key: 'banana',
+ label: 'Banana'
+ },
+ {
+ key: 'lemon',
+ label: 'Lemon'
+ },
+ ];
+
+ function App() {
+ const initialValues = {
+ fruits: {
+ apple: false,
+ banana: false,
+ lemon: false
+ }
+ };
+
+ return (
+
+ );
+ }
+
+ render();
+ `);
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default CheckboxGroupPage;
diff --git a/website/src/pages/components/checkbox-group/resources.mdx b/website/src/pages/components/checkbox-group/resources.mdx
new file mode 100644
index 00000000..72ae973f
--- /dev/null
+++ b/website/src/pages/components/checkbox-group/resources.mdx
@@ -0,0 +1,8 @@
+CheckboxGroup resources are coming soon.
+
+ Meantime, enjoy the{" "}
+
+ Playground
+
+ .
+
diff --git a/website/src/pages/components/checkbox-group/usage.mdx b/website/src/pages/components/checkbox-group/usage.mdx
new file mode 100644
index 00000000..9cdf7710
--- /dev/null
+++ b/website/src/pages/components/checkbox-group/usage.mdx
@@ -0,0 +1,8 @@
+CheckboxGroup usage is coming soon.
+
+ Meantime, enjoy the{" "}
+
+ Playground
+
+ .
+
diff --git a/website/src/pages/components/form/index.js b/website/src/pages/components/form/index.js
index 6c1f23eb..3bf92f32 100644
--- a/website/src/pages/components/form/index.js
+++ b/website/src/pages/components/form/index.js
@@ -55,6 +55,20 @@ function FormPage() {
value: "chernobyl"
}
];
+ const fruitOptions = [
+ {
+ key: "apple",
+ label: "Apple",
+ },
+ {
+ key: "banana",
+ label: "Banana",
+ },
+ {
+ key: "lemon",
+ label: "Lemon",
+ },
+ ];
const hungryOptions = [
{
label: "Yes",
@@ -106,6 +120,11 @@ function FormPage() {
name: "",
relationshipStatus: "",
favouriteMovie: "",
+ fruits: {
+ apple: false,
+ banana: false,
+ lemon: false,
+ },
likeIceCream: false,
hungry: "",
salary: {
@@ -141,9 +160,10 @@ function FormPage() {
+
I like ice cream
-
+
diff --git a/website/src/pages/kitchen-sink/components/checkbox-group.js b/website/src/pages/kitchen-sink/components/checkbox-group.js
new file mode 100644
index 00000000..369dd24f
--- /dev/null
+++ b/website/src/pages/kitchen-sink/components/checkbox-group.js
@@ -0,0 +1,157 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Container, Flex, Placeholder, CheckboxGroup, Stack } from "basis";
+import KitchenSinkLayout from "../../../components/kitchen-sink/KitchenSinkLayout";
+import KitchenSinkForm from "../../../components/kitchen-sink/KitchenSinkForm";
+
+const defaultOptions = [
+ {
+ key: "option1",
+ label: "Option 1",
+ },
+ {
+ key: "option2",
+ label: "Option 2",
+ },
+ {
+ key: "option3",
+ label: "Option 3",
+ },
+];
+
+function FormWithCheckboxGroup({
+ options = defaultOptions,
+ label,
+ initialValue = {
+ option1: false,
+ option2: false,
+ option3: false,
+ },
+ disabled,
+ helpText,
+ optional,
+ submitOnMount,
+}) {
+ return (
+
+
+
+ );
+}
+
+FormWithCheckboxGroup.propTypes = {
+ options: PropTypes.oneOfType([
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ })
+ ),
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ label: PropTypes.node.isRequired,
+ })
+ ),
+ ]),
+ label: PropTypes.string.isRequired,
+ initialValue: PropTypes.object,
+ disabled: PropTypes.bool,
+ helpText: PropTypes.string,
+ optional: PropTypes.bool,
+ submitOnMount: PropTypes.bool,
+};
+
+function KitchenSinkCheckboxGroup() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Option 1
+
+ ),
+ },
+ {
+ key: "value2",
+ label: (
+
+
+ Option 2
+
+ ),
+ },
+ {
+ key: "value3",
+ label: (
+
+
+ Option 3
+
+ ),
+ },
+ {
+ key: "value4",
+ label: (
+
+
+ Option 4
+
+ ),
+ },
+ ]}
+ />
+
+
+
+
+
+ );
+}
+
+export default KitchenSinkCheckboxGroup;