-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add checkbox and radioButton (#80)
Signed-off-by: Lukas.J.Han <[email protected]>
- Loading branch information
Showing
10 changed files
with
1,130 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.