From 020be02d7c85d6bc8d4e2bb1ef7a925800e10917 Mon Sep 17 00:00:00 2001 From: Robin Mulder <116208434+r-mulder@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:07:03 +0200 Subject: [PATCH] feat: add checkboxgroup component with story and extended the checkbox type (#2213) --- .changeset/calm-numbers-grin.md | 5 + .../components/Form/Checkbox/Checkbox.css.ts | 43 +++++- .../src/components/Form/Checkbox/Checkbox.tsx | 3 +- .../Form/Checkbox/CheckboxGroup.stories.tsx | 144 ++++++++++++++++++ .../Form/Checkbox/CheckboxGroup.tsx | 83 ++++++++++ 5 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 .changeset/calm-numbers-grin.md create mode 100644 packages/libs/react-ui/src/components/Form/Checkbox/CheckboxGroup.stories.tsx create mode 100644 packages/libs/react-ui/src/components/Form/Checkbox/CheckboxGroup.tsx diff --git a/.changeset/calm-numbers-grin.md b/.changeset/calm-numbers-grin.md new file mode 100644 index 0000000000..db80a8e31e --- /dev/null +++ b/.changeset/calm-numbers-grin.md @@ -0,0 +1,5 @@ +--- +"@kadena/react-ui": patch +--- + +Add checkboxgroup component diff --git a/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.css.ts b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.css.ts index 435c9f31b7..fe1814e58a 100644 --- a/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.css.ts +++ b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.css.ts @@ -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([ { @@ -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', @@ -20,7 +24,7 @@ export const labelClass = style([ }, }, }, - uiBaseRegular, + uiSmallRegular, ]); export const boxClass = style({ @@ -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'), +}); diff --git a/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx index e233222461..daae5dc283 100644 --- a/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx +++ b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx @@ -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, @@ -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; diff --git a/packages/libs/react-ui/src/components/Form/Checkbox/CheckboxGroup.stories.tsx b/packages/libs/react-ui/src/components/Form/Checkbox/CheckboxGroup.stories.tsx new file mode 100644 index 0000000000..5a7f27d417 --- /dev/null +++ b/packages/libs/react-ui/src/components/Form/Checkbox/CheckboxGroup.stories.tsx @@ -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 = { + 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; + +const checkboxes = Array.from(Array(10).keys()).map((key) => { + return ( + {`Option: ${key}`} + ); +}); + +export const Base: CheckboxGroupStoryType = { + args: { + direction: 'row', + }, + render: (props: ICheckboxProps) => { + return {checkboxes}; + }, +}; + +export const _Horizontal: CheckboxGroupStoryType = { + args: { + direction: 'row', + label: 'Label', + tag: 'tag', + info: 'info', + description: 'description', + }, + render: (props: ICheckboxProps) => { + return {checkboxes}; + }, +}; + +export const _Vertical: CheckboxGroupStoryType = { + args: { + direction: 'column', + label: 'Label', + tag: 'tag', + description: 'description', + }, + render: (props: ICheckboxProps) => { + return {checkboxes}; + }, +}; + +export const MultilineLabel: CheckboxGroupStoryType = { + args: { + direction: 'row', + label: 'Label', + tag: 'tag', + info: 'info', + description: 'description', + }, + render: (props: ICheckboxProps) => { + return ( + + + Hello this is a checkbox with a long label, probably enough words to + cause the checkbox label to be multiline + + + Hello this is a checkbox with a long label, probably enough words to + cause the checkbox label to be multiline + + Option: 3 + Option: 4 + + ); + }, +}; + +export default meta; diff --git a/packages/libs/react-ui/src/components/Form/Checkbox/CheckboxGroup.tsx b/packages/libs/react-ui/src/components/Form/Checkbox/CheckboxGroup.tsx new file mode 100644 index 0000000000..17bbee2c8a --- /dev/null +++ b/packages/libs/react-ui/src/components/Form/Checkbox/CheckboxGroup.tsx @@ -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>['direction']; + +export interface ICheckboxProps extends AriaCheckboxGroupProps { + children: ReactElement[] | ReactElement; + direction: Direction; + isReadOnly?: boolean; + label?: string; + tag?: string; + info?: string; +} + +export const CheckboxContext = createContext(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 ( +
+ {label && ( + + )} +
+ + {React.Children.map(children, (child) => + React.cloneElement(child, { isReadOnly }), + )} + +
+ {description && !isInvalid && ( + + {description} + + )} + {isInvalid && ( + + {error || (errorMessage as string)} + + )} +
+ ); +}