From 49fb388534865170085235586916c7e2272a683f Mon Sep 17 00:00:00 2001 From: Robin Mulder <116208434+r-mulder@users.noreply.github.com> Date: Tue, 28 May 2024 14:20:14 +0200 Subject: [PATCH] Feat/UI checkbox component (#2191) feat: add new checkbox component --- .changeset/shaggy-files-juggle.md | 5 + .../components/Form/Checkbox/Checkbox.css.ts | 101 ++++++++++++++++ .../Form/Checkbox/Checkbox.stories.tsx | 108 ++++++++++++++++++ .../src/components/Form/Checkbox/Checkbox.tsx | 55 +++++++++ .../react-ui/src/components/Form/index.ts | 1 + .../src/components/Link/Link.stories.tsx | 12 -- .../libs/react-ui/src/components/index.ts | 2 + packages/libs/react-ui/src/index.ts | 2 + 8 files changed, 274 insertions(+), 12 deletions(-) create mode 100644 .changeset/shaggy-files-juggle.md create mode 100644 packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.css.ts create mode 100644 packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.stories.tsx create mode 100644 packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx diff --git a/.changeset/shaggy-files-juggle.md b/.changeset/shaggy-files-juggle.md new file mode 100644 index 0000000000..73a29f25a5 --- /dev/null +++ b/.changeset/shaggy-files-juggle.md @@ -0,0 +1,5 @@ +--- +"@kadena/react-ui": patch +--- + +Add new checkbox 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 new file mode 100644 index 0000000000..435c9f31b7 --- /dev/null +++ b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.css.ts @@ -0,0 +1,101 @@ +import { style } from '@vanilla-extract/css'; +import { token, uiBaseRegular } from '../../../styles'; + +export const labelClass = style([ + { + display: 'flex', + color: token('color.text.base.default'), + alignItems: 'flex-start', + lineHeight: token('size.n4'), + cursor: 'pointer', + gap: token('size.n2'), + transition: 'color 0.2s ease', + selectors: { + '&[data-disabled="true"]': { + cursor: 'not-allowed', + color: token('color.text.base.@disabled'), + }, + '&[data-readonly="true"]': { + cursor: 'unset', + }, + }, + }, + uiBaseRegular, +]); + +export const boxClass = style({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + borderRadius: token('radius.xs'), + borderStyle: 'solid', + borderWidth: token('border.hairline'), + borderColor: token('color.border.base.bold'), + backgroundColor: token('color.background.input.default'), + transition: 'background-color 0.2s, border-color 0.2s', + width: token('size.n4'), + height: token('size.n4'), + minWidth: token('size.n4'), + minHeight: token('size.n4'), + selectors: { + // hovered + [`${labelClass}[data-hovered="true"] &`]: { + backgroundColor: token('color.background.input.@hover'), + }, + // focused + [`${labelClass}[data-focus-visible="true"] &`]: { + outline: `2px solid ${token('color.border.tint.outline')}`, + outlineOffset: '1px', + backgroundColor: token('color.background.input.@focus'), + }, + // disabled + [`${labelClass}[data-disabled="true"] &`]: { + borderColor: token('color.border.base.@disabled'), + backgroundColor: token('color.background.input.@disabled'), + }, + // selected + '&[data-selected="true"]': { + borderColor: token('color.border.base.boldest'), + backgroundColor: token('color.background.input.inverse.default'), + }, + [`${labelClass}[data-hovered="true"] &[data-selected="true"]`]: { + backgroundColor: token('color.background.input.inverse.@hover'), + }, + [`${labelClass}[data-focus-visible="true"] &[data-selected="true"]`]: { + outline: `2px solid ${token('color.border.tint.outline')}`, + outlineOffset: '1px', + backgroundColor: token('color.background.input.inverse.@focus'), + }, + // readonly + [`${labelClass}[data-readonly="true"] &`]: { + borderColor: token('color.border.base.@disabled'), + }, + [`${labelClass}[data-readonly="true"] &[data-selected="true"]`]: { + backgroundColor: token('color.background.input.@disabled'), + }, + }, +}); + +export const iconClass = style({ + color: token('color.icon.base.inverse.default'), + opacity: 0, + height: token('size.n3'), + width: token('size.n3'), + selectors: { + // selected + [`${boxClass}[data-selected="true"] &`]: { + opacity: 1, + }, + // disabled + [`${labelClass}[data-disabled="true"] ${boxClass}[data-selected="true"] &`]: + { + color: token('color.icon.base.@disabled'), + }, + // readonly + [`${labelClass}[data-readonly="true"] ${boxClass}[data-selected="true"] &`]: + { + color: token('color.icon.base.default'), + }, + }, +}); diff --git a/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.stories.tsx b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.stories.tsx new file mode 100644 index 0000000000..fc7e4254e4 --- /dev/null +++ b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,108 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import type { ICheckboxProps } from './Checkbox'; +import { Checkbox } from './Checkbox'; + +const meta: Meta = { + title: 'Form/Checkbox', + parameters: { + status: { type: 'stable' }, + controls: { + hideNoControlsWarning: true, + sort: 'requiredFirst', + }, + docs: { + description: { + component: 'The checkbox component', + }, + }, + }, + argTypes: { + isDisabled: { + control: { + type: 'boolean', + }, + }, + isSelected: { + control: { + type: 'boolean', + }, + }, + isDeterminate: { + control: { + type: 'boolean', + }, + }, + isReadOnly: { + control: { + type: 'boolean', + }, + }, + }, +}; + +type CheckboxStoryType = StoryObj; + +export const Base: CheckboxStoryType = { + args: { + children: 'Check this box', + }, + render: (props: ICheckboxProps) => { + return {props.children}; + }, +}; + +export const Determinate: CheckboxStoryType = { + args: { + children: 'Check this box', + isDeterminate: true, + }, + render: (props: ICheckboxProps) => { + return {props.children}; + }, +}; + +export const Disabled: CheckboxStoryType = { + args: { + children: 'Check this box', + isDisabled: true, + }, + render: (props: ICheckboxProps) => { + return {props.children}; + }, +}; + +export const DisabledChecked: CheckboxStoryType = { + args: { + children: 'Check this box', + isDisabled: true, + isSelected: true, + }, + render: (props: ICheckboxProps) => { + return {props.children}; + }, +}; + +export const ReadOnly: CheckboxStoryType = { + args: { + children: 'Check this box', + isReadOnly: true, + }, + render: (props: ICheckboxProps) => { + return {props.children}; + }, +}; + +export const ReadOnlyChecked: CheckboxStoryType = { + args: { + children: 'Check this box', + isReadOnly: true, + isSelected: true, + }, + render: (props: ICheckboxProps) => { + return {props.children}; + }, +}; + +export default meta; diff --git a/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx new file mode 100644 index 0000000000..e233222461 --- /dev/null +++ b/packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx @@ -0,0 +1,55 @@ +import { MonoCheck, MonoRemove } from '@kadena/react-icons'; +import React, { useRef } from 'react'; +import { + VisuallyHidden, + mergeProps, + useCheckbox, + useFocusRing, + useHover, +} from 'react-aria'; +import { useToggleState } from 'react-stately'; +import { boxClass, iconClass, labelClass } from './Checkbox.css'; + +export interface ICheckboxProps { + children: string; + isDisabled?: boolean; + isSelected?: boolean; + isReadOnly?: boolean; + isDeterminate?: boolean; + onChange?: (isSelected: boolean) => void; +} + +export function Checkbox(props: ICheckboxProps) { + const state = useToggleState(props); + const ref = useRef(null); + const { inputProps, labelProps } = useCheckbox(props, state, ref); + const { isFocusVisible, focusProps } = useFocusRing(); + const { isHovered, hoverProps } = useHover(props); + + const { isDisabled, children, isDeterminate, isReadOnly } = props; + + const hovered = isHovered && !isDisabled && !isReadOnly; + + return ( + + ); +} diff --git a/packages/libs/react-ui/src/components/Form/index.ts b/packages/libs/react-ui/src/components/Form/index.ts index 0a06f41c15..58b7f8cf61 100644 --- a/packages/libs/react-ui/src/components/Form/index.ts +++ b/packages/libs/react-ui/src/components/Form/index.ts @@ -1,6 +1,7 @@ export { CopyButton } from './ActionButtons/CopyButton'; export { Form, type IFormProps } from './Form'; +export { Checkbox, type ICheckboxProps } from './Checkbox/Checkbox'; export { Combobox, ComboboxItem, diff --git a/packages/libs/react-ui/src/components/Link/Link.stories.tsx b/packages/libs/react-ui/src/components/Link/Link.stories.tsx index 6a11d5dfd6..0b5575d406 100644 --- a/packages/libs/react-ui/src/components/Link/Link.stories.tsx +++ b/packages/libs/react-ui/src/components/Link/Link.stories.tsx @@ -68,7 +68,6 @@ type LinkStory = StoryObj; export const _Button: LinkStory = { args: { children: 'Hello world', - variant: 'primary', }, render: (props: ILinkProps) => { return {props.children}; @@ -78,7 +77,6 @@ export const _Button: LinkStory = { export const StartIcon: LinkStory = { args: { children: 'Hello world', - variant: 'primary', startVisual: , }, render: (props: ILinkProps) => { @@ -89,7 +87,6 @@ export const StartIcon: LinkStory = { export const EndIcon: LinkStory = { args: { children: 'Hello world', - variant: 'primary', endVisual: , }, render: (props: ILinkProps) => { @@ -100,7 +97,6 @@ export const EndIcon: LinkStory = { export const WithAvatar: LinkStory = { args: { children: 'Hello world', - variant: 'primary', startVisual: , }, render: (props: ILinkProps) => { @@ -111,7 +107,6 @@ export const WithAvatar: LinkStory = { export const BadgeOnly: LinkStory = { args: { children: 'Hello world', - variant: 'primary', endVisual: ( 6 @@ -126,7 +121,6 @@ export const BadgeOnly: LinkStory = { export const BadgeAndEndIcon: LinkStory = { args: { children: 'Hello world', - variant: 'primary', endVisual: ( <> @@ -144,7 +138,6 @@ export const BadgeAndEndIcon: LinkStory = { export const BadgeAndStartIcon: LinkStory = { args: { children: 'Hello world', - variant: 'primary', startVisual: , endVisual: ( @@ -159,7 +152,6 @@ export const BadgeAndStartIcon: LinkStory = { export const IconOnly: LinkStory = { args: { - variant: 'primary', children: , }, render: (props: ILinkProps) => { @@ -169,7 +161,6 @@ export const IconOnly: LinkStory = { export const StartVisualLoading: LinkStory = { args: { - variant: 'primary', startVisual: , children: 'Hello world', isLoading: true, @@ -181,7 +172,6 @@ export const StartVisualLoading: LinkStory = { export const EndVisualLoading: LinkStory = { args: { - variant: 'primary', endVisual: , children: 'Hello world', isLoading: true, @@ -193,7 +183,6 @@ export const EndVisualLoading: LinkStory = { export const IconOnlyLoadingWithLabel: LinkStory = { args: { - variant: 'primary', children: , isLoading: true, loadingLabel: 'Loading...', @@ -205,7 +194,6 @@ export const IconOnlyLoadingWithLabel: LinkStory = { export const IconOnlyLoading: LinkStory = { args: { - variant: 'primary', children: , isLoading: true, loadingLabel: '', diff --git a/packages/libs/react-ui/src/components/index.ts b/packages/libs/react-ui/src/components/index.ts index ec2c513380..0147e94526 100644 --- a/packages/libs/react-ui/src/components/index.ts +++ b/packages/libs/react-ui/src/components/index.ts @@ -12,6 +12,7 @@ export type { IDialogProps, } from './Dialog'; export type { + ICheckboxProps, IComboboxProps, IFormProps, INumberFieldProps, @@ -71,6 +72,7 @@ export { } from './Dialog'; export { Divider } from './Divider/Divider'; export { + Checkbox, Combobox, ComboboxItem, CopyButton, diff --git a/packages/libs/react-ui/src/index.ts b/packages/libs/react-ui/src/index.ts index fb2ec1dc3a..5bb099af0b 100644 --- a/packages/libs/react-ui/src/index.ts +++ b/packages/libs/react-ui/src/index.ts @@ -6,6 +6,7 @@ export type { IBreadcrumbsProps, IButtonProps, ICardProps, + ICheckboxProps, IComboboxProps, IContentHeaderProps, IDialogContentProps, @@ -61,6 +62,7 @@ export { Button, Card, Cell, + Checkbox, Column, Combobox, ComboboxItem,