Skip to content

Commit

Permalink
feat: add checkbox and radioButton (#80)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas.J.Han <[email protected]>
  • Loading branch information
lukasjhan authored Aug 20, 2024
1 parent 61f799f commit dba7454
Show file tree
Hide file tree
Showing 10 changed files with 1,130 additions and 0 deletions.
18 changes: 18 additions & 0 deletions examples/core/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from 'react';
import './App.css';
import {
Heading,
Expand All @@ -7,11 +8,28 @@ import {
Detail,
Label,
Link,
RadioButtonGroup,
} from '@krds-ui/core';

function App() {
const [selectedValue, setSelectedValue] = useState('on');
return (
<>
<div>
<RadioButtonGroup
name="test-radio"
options={[
{ value: 'on', label: 'On' },
{ value: 'off', label: 'Off' },
{ value: 'intermediate', label: 'Intermediate' },
]}
selectedValue={selectedValue}
onChange={(value) => {
console.log(`Switched to ${value}`);
setSelectedValue(value);
}}
/>
</div>
<div>
<Display size="l">Display Large</Display>
</div>
Expand Down
145 changes: 145 additions & 0 deletions packages/core/lib/components/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';
import { Label } from './Label';

export type CheckboxStatus = 'on' | 'off' | 'intermediate';

export type CheckboxProps = {
status: CheckboxStatus;
onChange: (newStatus: CheckboxStatus) => void;
label?: string;
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
id: string;
};

const CheckIcon = () => (
<svg
width="100%"
height="100%"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20 6L9 17L4 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

const IntermediateIcon = () => (
<svg
width="100%"
height="100%"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 12H19"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

export const Checkbox: React.FC<CheckboxProps> = ({
status,
onChange,
label,
disabled = false,
size = 'md',
id,
}) => {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
};

const labelSizeClasses = {
sm: 's' as const,
md: 'm' as const,
lg: 'l' as const,
};

const handleChange = () => {
if (!disabled) {
const newStatus: CheckboxStatus = status === 'on' ? 'off' : 'on';
onChange(newStatus);
}
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleChange();
}
};

const baseClasses = `
inline-flex items-center justify-center border rounded cursor-pointer
${sizeClasses[size]} transition-all duration-300 ease-in-out`;

const stateClasses = disabled
? 'bg-gray-10 border-gray-30 text-gray-40 cursor-not-allowed'
: status === 'on'
? 'bg-primary border-primary text-gray-0'
: status === 'intermediate'
? 'bg-primary border-primary text-gray-0'
: 'bg-gray-0 border-gray-30 hover:border-primary';

return (
<div
className="flex items-center gap-3 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-colors duration-200 focus:outline-none"
tabIndex={disabled ? -1 : 0}
role="checkbox"
aria-checked={
status === 'on' ? 'true' : status === 'intermediate' ? 'mixed' : 'false'
}
aria-disabled={disabled}
onKeyDown={handleKeyDown}
>
<div
className={`${baseClasses} ${stateClasses} relative`}
onClick={handleChange}
>
<div
className="absolute inset-0 transition-opacity duration-300 ease-in-out"
style={{ opacity: status === 'on' ? 1 : 0 }}
>
<CheckIcon />
</div>
<div
className="absolute inset-0 transition-opacity duration-300 ease-in-out"
style={{ opacity: status === 'intermediate' ? 1 : 0 }}
>
<IntermediateIcon />
</div>
<input
type="checkbox"
id={id}
checked={status === 'on'}
onChange={handleChange}
disabled={disabled}
className="sr-only"
/>
</div>
{label && (
<Label
htmlFor={id}
size={labelSizeClasses[size]}
color={disabled ? 'gray-40' : 'gray-90'}
className={`${disabled ? 'cursor-not-allowed' : 'cursor-pointer'} transition-all duration-300 ease-in-out`}
>
{label}
</Label>
)}
</div>
);
};
111 changes: 111 additions & 0 deletions packages/core/lib/components/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { Label } from './Label';

export type ChipProps = {
label: string;
checked: boolean;
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
onChange: (newCheckedState: boolean) => void;
id: string;
};

export const Chip: React.FC<ChipProps> = ({
label,
checked,
disabled = false,
size = 'md',
onChange,
id,
}) => {
const sizeClasses = {
sm: 'px-4 h-8',
md: 'px-4 h-9',
lg: 'px-4 h-10',
};

const labelSize = {
sm: 's' as const,
md: 'm' as const,
lg: 'm' as const,
}[size];
const labelColor = disabled ? 'gray-50' : checked ? 'primary' : 'gray-90';

const baseClasses = `inline-flex items-center gap-2 rounded-3 border transition-colors duration-200 ${
sizeClasses[size]
}`;

const stateClasses = disabled
? 'bg-gray-20 text-gray-50 border-gray-30 cursor-not-allowed'
: checked
? 'bg-primary-5 text-primary border-primary hover:bg-primary-10 cursor-pointer'
: 'bg-gray-0 text-gray-70 border-gray-30 hover:bg-gray-20 cursor-pointer';

const iconClasses = disabled
? 'text-gray-40'
: checked
? 'text-primary'
: 'text-gray-40';

const iconSizes = {
sm: 'w-5 h-5',
md: 'w-6 h-6',
lg: 'w-7 h-7',
};

const handleClick = () => {
if (!disabled) {
onChange(!checked);
}
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
if (!disabled) {
onChange(!checked);
}
}
};

return (
<div
className={`${baseClasses} ${stateClasses}`}
onClick={handleClick}
onKeyDown={handleKeyDown}
role="checkbox"
aria-checked={checked}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
>
<input
type="checkbox"
id={id}
className="sr-only"
checked={checked}
disabled={disabled}
onChange={handleClick}
aria-hidden="true"
/>
<svg
className={`${iconSizes[size]} ${iconClasses}`}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M5 13l4 4L19 7"></path>
</svg>
<Label
htmlFor={id}
size={labelSize}
className={`${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
color={labelColor}
>
{label}
</Label>
</div>
);
};
Loading

0 comments on commit dba7454

Please sign in to comment.