Skip to content

Commit

Permalink
Feat/UI checkbox component (#2191)
Browse files Browse the repository at this point in the history
feat: add new checkbox component
  • Loading branch information
r-mulder committed May 28, 2024
1 parent 8b759a2 commit 49fb388
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-files-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kadena/react-ui": patch
---

Add new checkbox component
101 changes: 101 additions & 0 deletions packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.css.ts
Original file line number Diff line number Diff line change
@@ -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'),
},
},
});
Original file line number Diff line number Diff line change
@@ -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<ICheckboxProps> = {
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<ICheckboxProps>;

export const Base: CheckboxStoryType = {
args: {
children: 'Check this box',
},
render: (props: ICheckboxProps) => {
return <Checkbox {...props}>{props.children}</Checkbox>;
},
};

export const Determinate: CheckboxStoryType = {
args: {
children: 'Check this box',
isDeterminate: true,
},
render: (props: ICheckboxProps) => {
return <Checkbox {...props}>{props.children}</Checkbox>;
},
};

export const Disabled: CheckboxStoryType = {
args: {
children: 'Check this box',
isDisabled: true,
},
render: (props: ICheckboxProps) => {
return <Checkbox {...props}>{props.children}</Checkbox>;
},
};

export const DisabledChecked: CheckboxStoryType = {
args: {
children: 'Check this box',
isDisabled: true,
isSelected: true,
},
render: (props: ICheckboxProps) => {
return <Checkbox {...props}>{props.children}</Checkbox>;
},
};

export const ReadOnly: CheckboxStoryType = {
args: {
children: 'Check this box',
isReadOnly: true,
},
render: (props: ICheckboxProps) => {
return <Checkbox {...props}>{props.children}</Checkbox>;
},
};

export const ReadOnlyChecked: CheckboxStoryType = {
args: {
children: 'Check this box',
isReadOnly: true,
isSelected: true,
},
render: (props: ICheckboxProps) => {
return <Checkbox {...props}>{props.children}</Checkbox>;
},
};

export default meta;
55 changes: 55 additions & 0 deletions packages/libs/react-ui/src/components/Form/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<label
{...mergeProps(labelProps, hoverProps, focusProps)}
className={labelClass}
data-hovered={hovered}
data-disabled={isDisabled}
data-focus-visible={isFocusVisible}
data-readonly={isReadOnly}
>
<VisuallyHidden>
<input {...mergeProps(inputProps, focusProps)} ref={ref} />
</VisuallyHidden>
<span className={boxClass} data-selected={state.isSelected} aria-hidden>
{isDeterminate ? (
<MonoRemove className={iconClass} />
) : (
<MonoCheck className={iconClass} />
)}
</span>
{children}
</label>
);
}
1 change: 1 addition & 0 deletions packages/libs/react-ui/src/components/Form/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
12 changes: 0 additions & 12 deletions packages/libs/react-ui/src/components/Link/Link.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ type LinkStory = StoryObj<ILinkProps>;
export const _Button: LinkStory = {
args: {
children: 'Hello world',
variant: 'primary',
},
render: (props: ILinkProps) => {
return <Link {...props}>{props.children}</Link>;
Expand All @@ -78,7 +77,6 @@ export const _Button: LinkStory = {
export const StartIcon: LinkStory = {
args: {
children: 'Hello world',
variant: 'primary',
startVisual: <MonoChevronLeft />,
},
render: (props: ILinkProps) => {
Expand All @@ -89,7 +87,6 @@ export const StartIcon: LinkStory = {
export const EndIcon: LinkStory = {
args: {
children: 'Hello world',
variant: 'primary',
endVisual: <MonoChevronRight />,
},
render: (props: ILinkProps) => {
Expand All @@ -100,7 +97,6 @@ export const EndIcon: LinkStory = {
export const WithAvatar: LinkStory = {
args: {
children: 'Hello world',
variant: 'primary',
startVisual: <Avatar name="Robin Mulder" color="category3" />,
},
render: (props: ILinkProps) => {
Expand All @@ -111,7 +107,6 @@ export const WithAvatar: LinkStory = {
export const BadgeOnly: LinkStory = {
args: {
children: 'Hello world',
variant: 'primary',
endVisual: (
<Badge size="sm" style="inverse">
6
Expand All @@ -126,7 +121,6 @@ export const BadgeOnly: LinkStory = {
export const BadgeAndEndIcon: LinkStory = {
args: {
children: 'Hello world',
variant: 'primary',
endVisual: (
<>
<Badge size="sm" style="inverse">
Expand All @@ -144,7 +138,6 @@ export const BadgeAndEndIcon: LinkStory = {
export const BadgeAndStartIcon: LinkStory = {
args: {
children: 'Hello world',
variant: 'primary',
startVisual: <MonoChevronLeft />,
endVisual: (
<Badge size="sm" style="inverse">
Expand All @@ -159,7 +152,6 @@ export const BadgeAndStartIcon: LinkStory = {

export const IconOnly: LinkStory = {
args: {
variant: 'primary',
children: <MonoChevronRight />,
},
render: (props: ILinkProps) => {
Expand All @@ -169,7 +161,6 @@ export const IconOnly: LinkStory = {

export const StartVisualLoading: LinkStory = {
args: {
variant: 'primary',
startVisual: <MonoChevronRight />,
children: 'Hello world',
isLoading: true,
Expand All @@ -181,7 +172,6 @@ export const StartVisualLoading: LinkStory = {

export const EndVisualLoading: LinkStory = {
args: {
variant: 'primary',
endVisual: <MonoChevronRight />,
children: 'Hello world',
isLoading: true,
Expand All @@ -193,7 +183,6 @@ export const EndVisualLoading: LinkStory = {

export const IconOnlyLoadingWithLabel: LinkStory = {
args: {
variant: 'primary',
children: <MonoChevronRight />,
isLoading: true,
loadingLabel: 'Loading...',
Expand All @@ -205,7 +194,6 @@ export const IconOnlyLoadingWithLabel: LinkStory = {

export const IconOnlyLoading: LinkStory = {
args: {
variant: 'primary',
children: <MonoChevronRight />,
isLoading: true,
loadingLabel: '',
Expand Down
2 changes: 2 additions & 0 deletions packages/libs/react-ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
IDialogProps,
} from './Dialog';
export type {
ICheckboxProps,
IComboboxProps,
IFormProps,
INumberFieldProps,
Expand Down Expand Up @@ -71,6 +72,7 @@ export {
} from './Dialog';
export { Divider } from './Divider/Divider';
export {
Checkbox,
Combobox,
ComboboxItem,
CopyButton,
Expand Down
Loading

0 comments on commit 49fb388

Please sign in to comment.