From 58f38b07421d88089162e3bb38fc3255a455d7ce Mon Sep 17 00:00:00 2001 From: healtheloper Date: Thu, 30 Nov 2023 18:55:18 +0900 Subject: [PATCH] =?UTF-8?q?NumberBox=20=EA=B5=AC=ED=98=84=ED=96=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/NumberBox/Count.tsx | 31 ++++++ .../components/NumberBox/NumberBox.style.ts | 92 ++++++++++++++++++ .../src/components/NumberBox/NumberBox.tsx | 97 +++++++++++++++++++ .../components/NumberBox/NumberBoxContext.tsx | 43 ++++++++ .../components/NumberBox/NumberBoxIcon.tsx | 9 ++ .../src/components/NumberBox/index.ts | 3 + .../NumberBox/stories/NumberBox.stories.tsx | 89 +++++++++++++++++ .../Radio/stories/Radio.stories.tsx | 1 - 8 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 packages/co-design-core/src/components/NumberBox/Count.tsx create mode 100644 packages/co-design-core/src/components/NumberBox/NumberBox.style.ts create mode 100644 packages/co-design-core/src/components/NumberBox/NumberBox.tsx create mode 100644 packages/co-design-core/src/components/NumberBox/NumberBoxContext.tsx create mode 100644 packages/co-design-core/src/components/NumberBox/NumberBoxIcon.tsx create mode 100644 packages/co-design-core/src/components/NumberBox/index.ts create mode 100644 packages/co-design-core/src/components/NumberBox/stories/NumberBox.stories.tsx diff --git a/packages/co-design-core/src/components/NumberBox/Count.tsx b/packages/co-design-core/src/components/NumberBox/Count.tsx new file mode 100644 index 00000000..eff7debf --- /dev/null +++ b/packages/co-design-core/src/components/NumberBox/Count.tsx @@ -0,0 +1,31 @@ +import { RefObject, forwardRef, useImperativeHandle, useState } from 'react'; +import { Typography, TypographyProps } from '../Typography'; +import { useNumberBoxContext } from './NumberBoxContext'; +import { useUpdateEffect } from '@co-design/hooks'; + +export interface CountRef { + count: number; +} + +interface CountProps extends TypographyProps<'span'> {} + +export const Count = forwardRef((props: CountProps, ref: RefObject) => { + const { isDecrease, currentCount, deletedCount } = useNumberBoxContext(); + const [count, setCount] = useState(currentCount); + + useImperativeHandle(ref, () => ({ + count, + })); + + useUpdateEffect(() => { + if (isDecrease && count >= deletedCount) { + setCount((prev) => prev - 1); + } + }, [currentCount]); + + return ( + + {count} + + ); +}); diff --git a/packages/co-design-core/src/components/NumberBox/NumberBox.style.ts b/packages/co-design-core/src/components/NumberBox/NumberBox.style.ts new file mode 100644 index 00000000..f30aa184 --- /dev/null +++ b/packages/co-design-core/src/components/NumberBox/NumberBox.style.ts @@ -0,0 +1,92 @@ +import { createStyles } from '@co-design/styles'; + +interface NumberBoxStyles { + checked?: boolean; + disabled?: boolean; + size?: 'small' | 'medium'; +} + +export default createStyles((theme, { checked, disabled, size }: NumberBoxStyles, getRef) => { + const checkmark = getRef('checkmark'); + const wrapper = getRef('wrapper'); + + const positionSizes = { + small: theme.foundations.tokens.size.size_02, + medium: theme.foundations.tokens.size.size_04, + }; + + const iconSizes = { + small: theme.foundations.tokens.size.size_08, + medium: theme.foundations.tokens.size.size_09, + }; + + return { + wrapper: { + ref: wrapper, + position: 'relative', + cursor: 'pointer', + userSelect: 'none', + verticalAlign: 'middle', + zIndex: 0, + width: 'fit-content', + ...(!disabled + ? { + '&:hover': { + [`.${checkmark}`]: { + fill: checked ? theme.foundations.tokens.color.bg.bg_primary_hover : theme.foundations.tokens.color.bg.bg_base_light, + }, + }, + '&:focus': { + [`.${checkmark}`]: { + color: theme.foundations.tokens.color.border.border_primary, + }, + }, + } + : {}), + }, + input: { + position: 'absolute', + width: '0px', + height: '0px', + opacity: 0, + }, + childrenWrapper: { + position: 'relative', + width: 'fit-content', + }, + checkWrapper: { + position: 'absolute', + top: theme.fn.size({ size, sizes: positionSizes }), + right: theme.fn.size({ size, sizes: positionSizes }), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + checkmark: { + ref: checkmark, + color: theme.foundations.tokens.color.border.border_strong, + fill: theme.foundations.tokens.color.bg.bg_surface_01, + width: theme.fn.size({ size, sizes: iconSizes }), + height: theme.fn.size({ size, sizes: iconSizes }), + }, + checked: { + [`.${checkmark}`]: { + color: theme.foundations.tokens.color.icon.icon_inverse_white, + fill: theme.foundations.tokens.color.bg.bg_primary, + }, + }, + disabled: { + cursor: 'not-allowed', + [`.${checkmark}`]: { + // TODO: replace with color token + color: checked ? 'rgba(0,0,0,0)' : theme.foundations.tokens.color.border.border_disabled, + fill: theme.foundations.tokens.color.bg.bg_disabled, + }, + }, + number: { + position: 'absolute', + color: disabled ? theme.foundations.tokens.color.icon.icon_disabled : theme.foundations.tokens.color.text.text_light, + opacity: checked ? 1 : 0, + }, + }; +}); diff --git a/packages/co-design-core/src/components/NumberBox/NumberBox.tsx b/packages/co-design-core/src/components/NumberBox/NumberBox.tsx new file mode 100644 index 00000000..a2d81b5f --- /dev/null +++ b/packages/co-design-core/src/components/NumberBox/NumberBox.tsx @@ -0,0 +1,97 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { ClassNames, CoComponentProps } from '@co-design/styles'; +import { NumberBoxIcon } from './NumberBoxIcon'; +import { View } from '../View'; +import useStyles from './NumberBox.style'; +import { Count, CountRef } from './Count'; +import { useNumberBoxContext } from './NumberBoxContext'; + +export type NumberBoxStylesNames = ClassNames; + +export interface NumberBoxProps extends CoComponentProps, Omit, 'onClick' | 'size'> { + /** NumberBox 를 감싸는 요소를 클릭했을 때 발생할 이벤트를 지정합니다. */ + onClick?: React.MouseEventHandler; + + size?: 'small' | 'medium'; + + /** NumberBox 를 감싸는 요소에 속성을 전달합니다. */ + wrapperProps?: React.ComponentPropsWithoutRef<'label'> & { [key: string]: any }; +} + +export const NumberBox = ({ + name, + value, + checked = false, + disabled = false, + onChange, + onClick, + size = 'small', + className = '', + style, + wrapperProps, + co, + overrideStyles, + children, + ...props +}: NumberBoxProps) => { + const { actions } = useNumberBoxContext(); + const countRef = useRef({ count: 0 }); + + const [check, setCheck] = useState(checked); + const { classes, cx } = useStyles( + { checked: check, disabled, size }, + { + overrideStyles, + name: 'NumberBox', + }, + ); + + const handleChange = (event: React.ChangeEvent) => { + const checked = event.target.checked; + + if (checked) { + actions.increase(); + } else { + actions.delete(countRef.current.count); + } + + setCheck(checked); + onChange?.(event); + }; + + useEffect(() => { + setCheck(checked); + }, [checked]); + + return ( + + + + {children} + + + {check && } + + + + ); +}; diff --git a/packages/co-design-core/src/components/NumberBox/NumberBoxContext.tsx b/packages/co-design-core/src/components/NumberBox/NumberBoxContext.tsx new file mode 100644 index 00000000..7805c218 --- /dev/null +++ b/packages/co-design-core/src/components/NumberBox/NumberBoxContext.tsx @@ -0,0 +1,43 @@ +import { createContext, ReactNode, useContext, useState } from 'react'; + +interface NumberBoxActions { + increase: () => void; + delete: (count: number) => void; +} + +type INumberBoxContext = { isDecrease: boolean; currentCount: number; deletedCount: number; actions: NumberBoxActions }; + +const NumberBoxContext = createContext(undefined as unknown as INumberBoxContext); + +export const useNumberBoxContext = () => { + const context = useContext(NumberBoxContext); + if (!context) { + throw new Error('useMe must be used within a NumberBoxProvider'); + } + return context; +}; + +interface Props { + children: ReactNode; +} + +export const NumberBoxProvider = ({ children }: Props) => { + const [currentCount, setCurrentCount] = useState(0); + const [deletedCount, setDeletedCount] = useState(); + const [isDecrease, setIsDecrease] = useState(false); + + const actions = { + increase: () => { + setCurrentCount((prev) => prev + 1); + setDeletedCount(undefined); + setIsDecrease(false); + }, + delete: (count: number) => { + setCurrentCount((prev) => prev - 1); + setIsDecrease(true); + setDeletedCount(count); + }, + }; + + return {children}; +}; diff --git a/packages/co-design-core/src/components/NumberBox/NumberBoxIcon.tsx b/packages/co-design-core/src/components/NumberBox/NumberBoxIcon.tsx new file mode 100644 index 00000000..0f2f10f5 --- /dev/null +++ b/packages/co-design-core/src/components/NumberBox/NumberBoxIcon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from 'react'; + +export const NumberBoxIcon = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/packages/co-design-core/src/components/NumberBox/index.ts b/packages/co-design-core/src/components/NumberBox/index.ts new file mode 100644 index 00000000..2210e241 --- /dev/null +++ b/packages/co-design-core/src/components/NumberBox/index.ts @@ -0,0 +1,3 @@ +export { NumberBox } from './NumberBox'; + +export type { NumberBoxProps } from './NumberBox'; diff --git a/packages/co-design-core/src/components/NumberBox/stories/NumberBox.stories.tsx b/packages/co-design-core/src/components/NumberBox/stories/NumberBox.stories.tsx new file mode 100644 index 00000000..35c859c6 --- /dev/null +++ b/packages/co-design-core/src/components/NumberBox/stories/NumberBox.stories.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { NumberBox } from '../NumberBox'; +import { NumberBoxProvider } from '../NumberBoxContext'; +import { Stack } from '../../Stack'; +import { Button } from '../../Button'; + +type Story = StoryObj; + +export default { + title: '@co-design/core/NumberBox', + component: NumberBox, + argTypes: { + checked: { + control: { type: 'boolean' }, + }, + disabled: { + control: { type: 'boolean' }, + }, + size: { + options: ['small', 'medium'], + control: { type: 'inline-radio' }, + }, + }, +} as Meta; + +export const Default: Story = { + render: (props) => { + return ( + + + + + + ); + }, +}; + +export const MultiSelect: Story = { + render: (props) => { + return ( + + + + + + + + + + + + + + + + + + + + ); + }, +}; + +export const Disabled: Story = { + render: (props) => { + const [disabled, setDisabled] = useState(false); + + return ( + + + + + + + + + + + + ); + }, +}; diff --git a/packages/co-design-core/src/components/Radio/stories/Radio.stories.tsx b/packages/co-design-core/src/components/Radio/stories/Radio.stories.tsx index 5e42079d..1667bc4a 100644 --- a/packages/co-design-core/src/components/Radio/stories/Radio.stories.tsx +++ b/packages/co-design-core/src/components/Radio/stories/Radio.stories.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import Radio from '../Radio'; import { Meta, StoryObj } from '@storybook/react';