Skip to content

Commit

Permalink
Merge pull request #12 from yourssu/feat/toggle
Browse files Browse the repository at this point in the history
feat: Toggle 구현
  • Loading branch information
nijuy authored Oct 17, 2023
2 parents 1b80cab + 04bffc4 commit 59f7b91
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 0 deletions.
49 changes: 49 additions & 0 deletions src/components/Toggle/Toggle.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Meta, StoryObj } from '@storybook/react';

import { Toggle } from './Toggle';

const meta: Meta<typeof Toggle> = {
title: 'Atoms/Toggle',
component: Toggle,
parameters: {
layout: 'centered',
},
};
export default meta;

const ToggleStory = ({ ...toggleProps }) => {
return <Toggle {...toggleProps} />;
};

type Story = StoryObj<typeof Toggle>;
export const Primary: Story = {
args: {
isDisabled: false,
isSelected: false,
},
render: ToggleStory,
};

export const Selected: Story = {
args: {
isDisabled: false,
isSelected: true,
},
render: ToggleStory,
};

export const Disabled: Story = {
args: {
isDisabled: true,
isSelected: false,
},
render: ToggleStory,
};

export const DisabledSelected: Story = {
args: {
isDisabled: true,
isSelected: true,
},
render: ToggleStory,
};
102 changes: 102 additions & 0 deletions src/components/Toggle/Toggle.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { styled } from 'styled-components';
import { DefaultTheme } from 'styled-components/dist/types';

import { ToggleProps } from './Toggle.type';

interface StyledToggleProps {
$isDisabled: ToggleProps['isDisabled'];
$isSelected: ToggleProps['isSelected'];
}

const TRACK_WIDTH = 51;
const TRACK_HEIGHT = 31;
const THUMB_SIZE = 27;
const PADDING = 2;
const MIN_WIDTH = 0.1;

const setTrackColor = ({
$isDisabled,
$isSelected,
theme,
}: {
$isDisabled: StyledToggleProps['$isDisabled'];
$isSelected: StyledToggleProps['$isSelected'];
theme: DefaultTheme;
}) => {
if ($isDisabled) return theme.color.buttonBG;
if ($isSelected) return theme.color.buttonPoint;
else return theme.color.buttonBG;
};

export const StyledInput = styled.input`
display: none;
`;

export const StyledThumb = styled.span<StyledToggleProps>`
display: inline-block;
width: ${THUMB_SIZE}px;
height: ${THUMB_SIZE}px;
border-radius: 50%;
background-color: ${({ theme, $isDisabled }) =>
$isDisabled ? theme.color.buttonDisabled : theme.color.buttonBright};
transform: ${({ $isSelected }) =>
$isSelected && `translateX(${TRACK_WIDTH - 2 * PADDING - THUMB_SIZE}px)`};
transition: 100ms ease-in-out;
&::before {
position: absolute;
content: '';
border-radius: inherit;
width: ${THUMB_SIZE + MIN_WIDTH}px;
height: ${THUMB_SIZE + MIN_WIDTH}px;
background-color: ${({ theme }) => theme.color.borderNormal};
}
&::after {
position: absolute;
content: '';
top: ${MIN_WIDTH}px;
left: ${MIN_WIDTH}px;
border-radius: inherit;
width: inherit;
height: inherit;
background-color: inherit;
}
`;

export const StyledTrack = styled.label<StyledToggleProps>`
display: inline-block;
width: ${TRACK_WIDTH}px;
height: ${TRACK_HEIGHT}px;
padding: ${PADDING}px;
border-radius: 15px;
background-color: ${({ theme, $isDisabled, $isSelected }) =>
setTrackColor({ $isDisabled, $isSelected, theme })};
cursor: ${({ $isDisabled }) => ($isDisabled ? 'not-allowed' : 'pointer')};
${StyledInput}:checked + ${StyledThumb} {
transform: translateX(${TRACK_WIDTH - 2 * PADDING - THUMB_SIZE}px);
}
${StyledInput} + ${StyledThumb} {
transform: translateX(0px);
}
`;

export const StyledToggle = styled.div`
${StyledInput}:checked + ${StyledTrack} {
background-color: ${({ theme }) => theme.color.buttonPoint};
}
${StyledInput}:disabled + ${StyledTrack} {
background-color: ${({ theme }) => theme.color.buttonBG};
}
${StyledInput} + ${StyledTrack} {
background-color: ${({ theme }) => theme.color.buttonBG};
}
${StyledInput}:checked + ${StyledTrack} > ${StyledThumb} {
transform: translateX(${TRACK_WIDTH - 2 * PADDING - THUMB_SIZE}px);
}
${StyledInput} + ${StyledTrack} > ${StyledThumb} {
transform: translateX(0px);
}
`;
41 changes: 41 additions & 0 deletions src/components/Toggle/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { MouseEvent, forwardRef, useImperativeHandle, useRef } from 'react';

import { StyledToggle, StyledInput, StyledTrack, StyledThumb } from './Toggle.style';
import { ToggleProps } from './Toggle.type';

export const Toggle = forwardRef<HTMLDivElement, ToggleProps>(
({ isDisabled = false, isSelected = false, ...props }, ref) => {
const toggleRef = useRef<HTMLInputElement | null>(null);
useImperativeHandle(ref, () => toggleRef.current as HTMLInputElement);

const handleToggleClick = (event: MouseEvent) => {
event.preventDefault();

if (isDisabled) return;
if (toggleRef.current) {
toggleRef.current.checked = !toggleRef.current.checked;
}
};

return (
<StyledToggle
ref={ref}
onClick={(event) => {
handleToggleClick(event);
}}
>
<StyledInput
ref={toggleRef}
type="checkbox"
disabled={isDisabled}
checked={isSelected}
{...props}
readOnly
/>
<StyledTrack $isDisabled={isDisabled} $isSelected={isSelected}>
<StyledThumb $isDisabled={isDisabled} $isSelected={isSelected} />
</StyledTrack>
</StyledToggle>
);
}
);
4 changes: 4 additions & 0 deletions src/components/Toggle/Toggle.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ToggleProps extends React.InputHTMLAttributes<HTMLInputElement> {
isDisabled?: boolean;
isSelected?: boolean;
}
2 changes: 2 additions & 0 deletions src/components/Toggle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Toggle } from './Toggle';
export type { ToggleProps } from './Toggle.type';

0 comments on commit 59f7b91

Please sign in to comment.