Skip to content

Commit

Permalink
feat: add checkboxgroup component with story and extended the checkbo…
Browse files Browse the repository at this point in the history
…x type (#2213)
  • Loading branch information
r-mulder committed Jun 4, 2024
1 parent 60e6d60 commit 020be02
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/calm-numbers-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kadena/react-ui": patch
---

Add checkboxgroup component
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { style } from '@vanilla-extract/css';
import { token, uiBaseRegular } from '../../../styles';
import { fallbackVar, style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
import { token, uiSmallRegular } from '../../../styles';

const maxWidth = fallbackVar('100%');

export const labelClass = style([
{
Expand All @@ -10,6 +13,7 @@ export const labelClass = style([
cursor: 'pointer',
gap: token('size.n2'),
transition: 'color 0.2s ease',
maxWidth: maxWidth,
selectors: {
'&[data-disabled="true"]': {
cursor: 'not-allowed',
Expand All @@ -20,7 +24,7 @@ export const labelClass = style([
},
},
},
uiBaseRegular,
uiSmallRegular,
]);

export const boxClass = style({
Expand Down Expand Up @@ -99,3 +103,36 @@ export const iconClass = style({
},
},
});

export const groupClass = recipe({
base: [
{
flexWrap: 'wrap',
display: 'flex',
},
],
variants: {
direction: {
row: {
flexDirection: 'row',
gap: `${token('spacing.n2')} ${token('spacing.n4')}`,
vars: {
[maxWidth]: '32%',
},
},
column: {
flexDirection: 'column',
gap: token('spacing.n2'),
vars: {
[maxWidth]: '100%',
},
},
},
},
});

export const layoutClass = style({
display: 'flex',
flexDirection: 'column',
gap: token('spacing.n2'),
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MonoCheck, MonoRemove } from '@kadena/react-icons';
import React, { useRef } from 'react';
import type { AriaCheckboxProps } from 'react-aria';
import {
VisuallyHidden,
mergeProps,
Expand All @@ -10,7 +11,7 @@ import {
import { useToggleState } from 'react-stately';
import { boxClass, iconClass, labelClass } from './Checkbox.css';

export interface ICheckboxProps {
export interface ICheckboxProps extends AriaCheckboxProps {
children: string;
isDisabled?: boolean;
isSelected?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { getVariants } from '../../../storyDecorators';
import { Checkbox } from './Checkbox';
import { groupClass } from './Checkbox.css';
import type { ICheckboxProps } from './CheckboxGroup';
import { CheckboxGroup } from './CheckboxGroup';

const directions = getVariants(groupClass);

const meta: Meta<ICheckboxProps> = {
title: 'Form/CheckboxGroup',
parameters: {
status: { type: 'stable' },
controls: {
hideNoControlsWarning: true,
sort: 'requiredFirst',
},
docs: {
description: {
component: 'The checkbox component',
},
},
},
argTypes: {
isDisabled: {
control: {
type: 'boolean',
},
},
direction: {
options: directions.direction,
control: {
type: 'select',
},
},
isReadOnly: {
control: {
type: 'boolean',
},
},
isInvalid: {
control: {
type: 'boolean',
},
},
label: {
control: {
type: 'text',
},
},
errorMessage: {
control: {
type: 'text',
},
},
description: {
control: {
type: 'text',
},
},
tag: {
control: {
type: 'text',
},
},
info: {
control: {
type: 'text',
},
},
},
};

type CheckboxGroupStoryType = StoryObj<ICheckboxProps>;

const checkboxes = Array.from(Array(10).keys()).map((key) => {
return (
<Checkbox key={key} value={`value:${key}`}>{`Option: ${key}`}</Checkbox>
);
});

export const Base: CheckboxGroupStoryType = {
args: {
direction: 'row',
},
render: (props: ICheckboxProps) => {
return <CheckboxGroup {...props}>{checkboxes}</CheckboxGroup>;
},
};

export const _Horizontal: CheckboxGroupStoryType = {
args: {
direction: 'row',
label: 'Label',
tag: 'tag',
info: 'info',
description: 'description',
},
render: (props: ICheckboxProps) => {
return <CheckboxGroup {...props}>{checkboxes}</CheckboxGroup>;
},
};

export const _Vertical: CheckboxGroupStoryType = {
args: {
direction: 'column',
label: 'Label',
tag: 'tag',
description: 'description',
},
render: (props: ICheckboxProps) => {
return <CheckboxGroup {...props}>{checkboxes}</CheckboxGroup>;
},
};

export const MultilineLabel: CheckboxGroupStoryType = {
args: {
direction: 'row',
label: 'Label',
tag: 'tag',
info: 'info',
description: 'description',
},
render: (props: ICheckboxProps) => {
return (
<CheckboxGroup {...props}>
<Checkbox value="something">
Hello this is a checkbox with a long label, probably enough words to
cause the checkbox label to be multiline
</Checkbox>
<Checkbox value="something else">
Hello this is a checkbox with a long label, probably enough words to
cause the checkbox label to be multiline
</Checkbox>
<Checkbox value="3">Option: 3</Checkbox>
<Checkbox value="4">Option: 4</Checkbox>
</CheckboxGroup>
);
},
};

export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { RecipeVariants } from '@vanilla-extract/recipes';
import type { ReactElement } from 'react';
import React, { createContext } from 'react';
import type { AriaCheckboxGroupProps } from 'react-aria';
import { useCheckboxGroup } from 'react-aria';
import type { CheckboxGroupState } from 'react-stately';
import { useCheckboxGroupState } from 'react-stately';
import { FormFieldHeader } from '../FormFieldHeader/FormFieldHeader';
import { FormFieldHelpText } from '../FormFieldHelpText/FormFieldHelpText';
import { groupClass, layoutClass } from './Checkbox.css';

type Direction = NonNullable<RecipeVariants<typeof groupClass>>['direction'];

export interface ICheckboxProps extends AriaCheckboxGroupProps {
children: ReactElement[] | ReactElement;
direction: Direction;
isReadOnly?: boolean;
label?: string;
tag?: string;
info?: string;
}

export const CheckboxContext = createContext<CheckboxGroupState | null>(null);

export function CheckboxGroup(props: ICheckboxProps) {
const {
children,
description,
direction = 'row',
errorMessage,
isDisabled,
isInvalid = false,
isReadOnly,
info,
label,
tag,
} = props;
const state = useCheckboxGroupState(props);
const {
descriptionProps,
groupProps,
errorMessageProps,
labelProps,
validationDetails,
validationErrors,
} = useCheckboxGroup(props, state);

const error =
typeof errorMessage === 'function'
? errorMessage({ isInvalid, validationErrors, validationDetails })
: errorMessage ?? validationErrors.join(' ');

return (
<div {...groupProps} className={layoutClass}>
{label && (
<FormFieldHeader
label={label}
tag={tag}
info={info}
isDisabled={isDisabled}
{...labelProps}
/>
)}
<div className={groupClass({ direction })}>
<CheckboxContext.Provider value={state}>
{React.Children.map(children, (child) =>
React.cloneElement(child, { isReadOnly }),
)}
</CheckboxContext.Provider>
</div>
{description && !isInvalid && (
<FormFieldHelpText {...descriptionProps}>
{description}
</FormFieldHelpText>
)}
{isInvalid && (
<FormFieldHelpText {...errorMessageProps} intent="negative">
{error || (errorMessage as string)}
</FormFieldHelpText>
)}
</div>
);
}

0 comments on commit 020be02

Please sign in to comment.