Skip to content

Commit

Permalink
Add CheckboxGroup (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
moroshko committed Apr 26, 2021
1 parent f867799 commit 2708d95
Show file tree
Hide file tree
Showing 12 changed files with 557 additions and 13 deletions.
172 changes: 172 additions & 0 deletions src/components/CheckboxGroup.js
Original file line number Diff line number Diff line change
@@ -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 (
<Field
optional={optional}
disabled={disabled}
label={label}
labelId={labelId}
auxId={auxId}
helpText={helpText}
errors={errors}
testId={testId}
>
<Stack gap="1">
{options.map(({ key, label }) => (
<InternalCheckbox
name={`${name}.${key}`}
parentName={name}
color={props.color}
disabled={disabled}
isValid={!hasErrors}
onFocus={onFocus}
onBlur={onBlur}
onMouseDown={onMouseDown}
value={value[key]}
onChange={onChange}
key={key}
>
{label}
</InternalCheckbox>
))}
</Stack>
</Field>
);
}

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;
5 changes: 4 additions & 1 deletion src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]);
Expand Down
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
7 changes: 4 additions & 3 deletions src/components/internal/InternalCheckbox.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -65,7 +66,6 @@ function InternalCheckbox(_props) {
const {
name,
parentName,
inputId,
color,
disabled,
isValid,
Expand All @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions src/utils/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions website/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ module.exports = {
Checkbox: {
status: COMPONENT_STATUS.READY,
},
CheckboxGroup: {
status: COMPONENT_STATUS.READY,
},
Container: {
status: COMPONENT_STATUS.READY,
},
Expand Down
19 changes: 11 additions & 8 deletions website/src/components/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ function Sidebar() {
query ComponentsQuery {
allFile(
filter: { relativePath: { regex: "/^components/.*/index.js/" } }
sort: { order: ASC, fields: relativePath }
) {
edges {
node {
Expand All @@ -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 (
<header css={{ minHeight: 0, display: "flex", flexDirection: "column" }}>
Expand Down
Loading

1 comment on commit 2708d95

@vercel
Copy link

@vercel vercel bot commented on 2708d95 Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.