From 4d7115a1d2b7a4b5112e5ea879334cff90550781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=98=84=EC=98=81?= <89445100+hamo-o@users.noreply.github.com> Date: Tue, 2 Jul 2024 03:41:11 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20Button=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 버튼 기본 틀 작성 * fix: style 변수명 변경 * feat: 다형성 고려하기 * feat: Enter 키보드 이벤트 추가 * feat: useButton hook 분리 * chore: 속성 정의 * design: disabled되지 않았을 때만 hover 스타일 적용 * feat: shadows token 등록 * feat: 기본 Button 상태 스타일 정리 * feat: hover pseudo class 재정의 * chore: Button 파일명 변경 & sm padding 변경 * feat: TextButton 컴포넌트 구현 * fix: codegen 관련 오류 수정 * feat: Button 스토리북 작성 * feat: TextButton disabled 상태에서의 커서 변경 * chore: Button props 순서 변경 * feat: TextButton 스토리북 작성 * fix: color contrast a11y 비활성화 * chore: pressed 관련 함수 네이밍 변경 * fix: pressed 이벤트 모바일에서도 적용될 수 있도록 이벤트 변경 * fix: TextButton 문서 누락 * design: PC에서만 버튼 너비 최솟값 지정 * fix: Button 컴포넌트의 label을 ReactNode를 받을 수 있게 수정, children으로 네이밍 변경 * chore: TextButton underline css 속성 적용 방법 변경 * feat: 내부 중복 이벤트 외부에서도 받도록 수정 * chore: TextButton label 네이밍 text로 수정 * fix: storybook args 이름 수정 --- packages/codegen/package.json | 1 + packages/theme/src/tokens/index.ts | 2 + packages/theme/src/tokens/shadows.ts | 7 + packages/wow-tokens/src/index.ts | 1 + packages/wow-tokens/src/shadow.ts | 17 ++ packages/wow-ui/package.json | 5 + packages/wow-ui/panda.config.ts | 3 + packages/wow-ui/rollup.config.js | 1 + .../src/components/Button/Button.stories.ts | 19 -- .../src/components/Button/Button.stories.tsx | 168 ++++++++++++++ .../src/components/Button/Button.test.tsx | 10 - .../wow-ui/src/components/Button/index.tsx | 217 ++++++++++++++++-- .../TextButton/TextButton.stories.tsx | 142 ++++++++++++ .../src/components/TextButton/index.tsx | 138 +++++++++++ packages/wow-ui/src/hooks/useButton.ts | 70 ++++++ packages/wow-ui/src/types/Polymorphic.ts | 9 +- 16 files changed, 760 insertions(+), 50 deletions(-) create mode 100644 packages/theme/src/tokens/shadows.ts create mode 100644 packages/wow-tokens/src/shadow.ts delete mode 100644 packages/wow-ui/src/components/Button/Button.stories.ts create mode 100644 packages/wow-ui/src/components/Button/Button.stories.tsx delete mode 100644 packages/wow-ui/src/components/Button/Button.test.tsx create mode 100644 packages/wow-ui/src/components/TextButton/TextButton.stories.tsx create mode 100644 packages/wow-ui/src/components/TextButton/index.tsx create mode 100644 packages/wow-ui/src/hooks/useButton.ts diff --git a/packages/codegen/package.json b/packages/codegen/package.json index 96117bd2..e48bb43e 100644 --- a/packages/codegen/package.json +++ b/packages/codegen/package.json @@ -1,6 +1,7 @@ { "name": "codegen", "private": true, + "type": "module", "devDependencies": { "plop": "^4.0.1" } diff --git a/packages/theme/src/tokens/index.ts b/packages/theme/src/tokens/index.ts index 38a2977f..ad922f66 100644 --- a/packages/theme/src/tokens/index.ts +++ b/packages/theme/src/tokens/index.ts @@ -6,6 +6,7 @@ import { defineTokens } from "@pandacss/dev"; import { colors, gradients } from "./color.ts"; import { radii } from "./radius.ts"; +import { shadows } from "./shadows.ts"; import { spacing } from "./space.ts"; import { borderWidths } from "./stroke.ts"; import { zIndex } from "./zIndex.ts"; @@ -17,4 +18,5 @@ export const tokens = defineTokens({ radii, borderWidths, zIndex, + shadows, }); diff --git a/packages/theme/src/tokens/shadows.ts b/packages/theme/src/tokens/shadows.ts new file mode 100644 index 00000000..a38876e8 --- /dev/null +++ b/packages/theme/src/tokens/shadows.ts @@ -0,0 +1,7 @@ +import { defineTokens } from "@pandacss/dev"; +import { shadow } from "wowds-tokens"; + +export const shadows = defineTokens.shadows({ + blue: { value: shadow.blue }, + mono: { value: shadow.mono }, +}); diff --git a/packages/wow-tokens/src/index.ts b/packages/wow-tokens/src/index.ts index 24e2b0d6..8c01423b 100644 --- a/packages/wow-tokens/src/index.ts +++ b/packages/wow-tokens/src/index.ts @@ -1,6 +1,7 @@ export * as breakpoint from "./breakpoint.ts"; export * as color from "./color.ts"; export * as radius from "./radius.ts"; +export * as shadow from "./shadow.ts"; export * as space from "./space.ts"; export * as stroke from "./stroke.ts"; export * as typography from "./typography.ts"; diff --git a/packages/wow-tokens/src/shadow.ts b/packages/wow-tokens/src/shadow.ts new file mode 100644 index 00000000..c2d6a276 --- /dev/null +++ b/packages/wow-tokens/src/shadow.ts @@ -0,0 +1,17 @@ +import { color } from "./index.ts"; + +export const blue = { + offsetX: 0, + offsetY: 4, + blur: 8, + spread: 0, + color: color.blueShadow, +}; + +export const mono = { + offsetX: 0, + offsetY: 4, + blur: 8, + spread: 0, + color: color.shadowMedium, +}; diff --git a/packages/wow-ui/package.json b/packages/wow-ui/package.json index 3ee32d31..eeb532d7 100644 --- a/packages/wow-ui/package.json +++ b/packages/wow-ui/package.json @@ -25,6 +25,11 @@ "require": "./dist/TextField.cjs", "import": "./dist/TextField.js" }, + "./TextButton": { + "types": "./dist/components/TextButton/index.d.ts", + "require": "./dist/TextButton.cjs", + "import": "./dist/TextButton.js" + }, "./Switch": { "types": "./dist/components/Switch/index.d.ts", "require": "./dist/Switch.cjs", diff --git a/packages/wow-ui/panda.config.ts b/packages/wow-ui/panda.config.ts index 10dc0afe..f4c979b6 100644 --- a/packages/wow-ui/panda.config.ts +++ b/packages/wow-ui/panda.config.ts @@ -44,4 +44,7 @@ export default defineConfig({ semanticTokens, breakpoints, }, + conditions: { + hover: "&[aria-pressed=false]:not(:disabled):hover", + }, }); diff --git a/packages/wow-ui/rollup.config.js b/packages/wow-ui/rollup.config.js index 6763c2cb..2866ca72 100644 --- a/packages/wow-ui/rollup.config.js +++ b/packages/wow-ui/rollup.config.js @@ -21,6 +21,7 @@ process.env.BABEL_ENV = "production"; export default { input: { TextField: "./src/components/TextField", + TextButton: "./src/components/TextButton", Switch: "./src/components/Switch", RadioButton: "./src/components/RadioGroup/RadioButton", RadioGroup: "./src/components/RadioGroup/RadioGroup", diff --git a/packages/wow-ui/src/components/Button/Button.stories.ts b/packages/wow-ui/src/components/Button/Button.stories.ts deleted file mode 100644 index d5b7d788..00000000 --- a/packages/wow-ui/src/components/Button/Button.stories.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import Button from "@/components/Button"; - -const meta = { - title: "Example/Button", - component: Button, - tags: ["autodocs"], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - children: "버튼", - }, -}; diff --git a/packages/wow-ui/src/components/Button/Button.stories.tsx b/packages/wow-ui/src/components/Button/Button.stories.tsx new file mode 100644 index 00000000..c19c9ef7 --- /dev/null +++ b/packages/wow-ui/src/components/Button/Button.stories.tsx @@ -0,0 +1,168 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Button from "@/components/Button"; + +const meta = { + title: "UI/Button", + component: Button, + tags: ["autodocs"], + parameters: { + componentSubtitle: "버튼 컴포넌트", + a11y: { + config: { + rules: [{ id: "color-contrast", enabled: false }], + }, + }, + }, + argTypes: { + label: { + description: "버튼의 라벨을 나타냅니다.", + table: { + type: { summary: "string" }, + }, + control: { + type: "text", + }, + }, + as: { + description: "버튼을 구성할 HTML 태그의 종류를 나타냅니다.", + table: { + type: { summary: "ElementType" }, + }, + control: false, + }, + disabled: { + description: "버튼이 비활성화되어 있는지 여부를 나타냅니다.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + control: { + type: "boolean", + }, + }, + size: { + description: "버튼의 크기를 나타냅니다.", + table: { + type: { summary: "lg | sm" }, + defaultValue: { summary: "lg" }, + }, + control: { + type: "radio", + options: ["lg", "sm"], + }, + }, + variant: { + description: "버튼의 종류를 나타냅니다.", + table: { + type: { summary: "solid | outline" }, + defaultValue: { summary: "solid" }, + }, + control: { + type: "radio", + options: ["solid", "outline"], + }, + }, + onKeyDown: { + description: + "버튼에 포커스 된 상태에서 엔터 키 또는 스페이스 바를 누르고 있는 동안 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + onKeyUp: { + description: + "버튼에 포커스 된 상태에서 엔터 키 또는 스페이스 바를 뗐을 때 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + onMouseLeave: { + description: + "버튼의 영역에서 마우스가 벗어났을 때 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + onPointerDown: { + description: + "버튼에 포커스 된 상태에서 마우스 또는 터치로 누르고 있는 동안 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + onPointerUp: { + description: + "버튼에 포커스 된 상태에서 마우스 또는 터치를 뗐을 때 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + style: { + description: "버튼의 커스텀 스타일을 나타냅니다.", + table: { + type: { summary: "CSSProperties" }, + }, + control: false, + }, + className: { + description: "버튼에 전달하는 커스텀 클래스를 나타냅니다.", + table: { + type: { summary: "string" }, + }, + control: false, + }, + ref: { + description: "렌더링된 요소 또는 컴포넌트에 연결할 ref를 나타냅니다.", + table: { + type: { summary: 'ComponentPropsWithRef["ref"]' }, + }, + control: false, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: { + children: "버튼", + }, +}; + +export const LargeSolid: Story = { + args: { + children: "버튼", + variant: "solid", + }, +}; + +export const LargeOutline: Story = { + args: { + children: "버튼", + variant: "outline", + }, +}; + +export const SmallSolid: Story = { + args: { + children: "버튼", + size: "sm", + variant: "solid", + }, +}; + +export const SmallOutline: Story = { + args: { + children: "버튼", + size: "sm", + variant: "outline", + }, +}; diff --git a/packages/wow-ui/src/components/Button/Button.test.tsx b/packages/wow-ui/src/components/Button/Button.test.tsx deleted file mode 100644 index a6e73d9c..00000000 --- a/packages/wow-ui/src/components/Button/Button.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { render } from "@testing-library/react"; - -import Button from "."; - -test("Button renders correctly", () => { - const { getByText } = render(); - - const buttonElement = getByText("Click me"); - expect(buttonElement).toBeTruthy(); -}); diff --git a/packages/wow-ui/src/components/Button/index.tsx b/packages/wow-ui/src/components/Button/index.tsx index afb23018..9210c7db 100644 --- a/packages/wow-ui/src/components/Button/index.tsx +++ b/packages/wow-ui/src/components/Button/index.tsx @@ -1,25 +1,206 @@ -import { css } from "@styled-system/css/css"; -import type { ReactNode } from "react"; +"use client"; -export interface ButtonProps { +import { cva } from "@styled-system/css/cva"; +import { styled } from "@styled-system/jsx"; +import type { CSSProperties, ElementType, ReactNode } from "react"; +import { forwardRef } from "react"; + +import useButton from "@/hooks/useButton"; +import type { + PolymorphicComponentProps, + PolymorphicComponentPropsWithRef, + PolymorphicRef, +} from "@/types"; + +/** + * @description 버튼 컴포넌트의 속성을 정의합니다. + * + * @param {ReactNode} children - 버튼의 자식 요소. + * @param {boolean} [disabled] - 버튼이 비활성화되어 있는지 여부. + * @param {"lg" | "sm"} [size] - 버튼의 크기. + * @param {"solid" | "outline"} [variant] - 버튼의 종류. + * @param {() => void} [onKeyUp] - 버튼에 포커스 된 상태에서 엔터 키 또는 스페이스 바를 뗐을 때 동작할 이벤트. + * @param {() => void} [onKeyDown] - 버튼에 포커스 된 상태에서 엔터 키 또는 스페이스 바를 누르고 있는 동안 동작할 이벤트. + * @param {() => void} [onMouseLeave] - 버튼의 영역에서 마우스가 벗어났을 때 동작할 이벤트. + * @param {() => void} [onPointerDown] - 버튼에 포커스 된 상태에서 마우스 또는 터치로 누르고 있는 동안 동작할 이벤트. + * @param {() => void} [onPointerUp] - 버튼에 포커스 된 상태에서 마우스 또는 터치를 뗐을 때 동작할 이벤트. + * @param {CSSProperties} [style] - 버튼의 커스텀 스타일. + * @param {string} [className] - 버튼에 전달하는 커스텀 클래스. + * @param {ComponentPropsWithoutRef} rest 렌더링된 요소 또는 컴포넌트에 전달할 추가 props. + * @param {ComponentPropsWithRef["ref"]} ref 렌더링된 요소 또는 컴포넌트에 연결할 ref. + */ + +export interface CustomButtonProps { children: ReactNode; + disabled?: boolean; + size?: "lg" | "sm"; + variant?: "solid" | "outline"; + onKeyUp?: () => void; + onKeyDown?: () => void; + onMouseLeave?: () => void; + onPointerDown?: () => void; + onPointerUp?: () => void; + style?: CSSProperties; + className?: string; } -const Button = ({ children }: ButtonProps) => { - return ( - - ); -}; + }, + sm: { + padding: "0.75rem 1.25rem", + borderRadius: "full", + }, + }, + variant: { + solid: { + background: "primary", + color: "textWhite", + + _disabled: { + background: "darkDisabled", + cursor: "not-allowed", + }, + _hover: { + shadow: "blue", + }, + _pressed: { + background: "bluePressed", + }, + }, + outline: { + borderWidth: 1, + borderStyle: "solid", + borderColor: "primary", + + background: "background", + color: "primary", + + _disabled: { + borderColor: "darkDisabled", + color: "darkDisabled", + cursor: "not-allowed", + }, + _hover: { + borderColor: "blueHover", + color: "blueHover", + }, + _pressed: { + borderColor: "bluePressed", + background: "blueBackgroundPressed", + color: "bluePressed", + }, + }, + }, + }, + compoundVariants: [ + { + size: "sm", + variant: "outline", + css: { + borderColor: "outline", + color: "textBlack", + + _hover: { + borderColor: "textBlack", + color: "textBlack", + }, + _pressed: { + borderColor: "outline", + background: "monoBackgroundPressed", + color: "textBlack", + }, + }, + }, + ], +}); +Button.displayName = "Button"; export default Button; diff --git a/packages/wow-ui/src/components/TextButton/TextButton.stories.tsx b/packages/wow-ui/src/components/TextButton/TextButton.stories.tsx new file mode 100644 index 00000000..8afd64e3 --- /dev/null +++ b/packages/wow-ui/src/components/TextButton/TextButton.stories.tsx @@ -0,0 +1,142 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import TextButton from "@/components/TextButton"; + +const meta = { + title: "UI/TextButton", + component: TextButton, + tags: ["autodocs"], + parameters: { + componentSubtitle: "텍스트 버튼 컴포넌트", + }, + argTypes: { + label: { + description: "텍스트 버튼의 라벨을 나타냅니다.", + table: { + type: { summary: "string" }, + }, + control: { + type: "text", + }, + }, + as: { + description: "텍스트 버튼을 구성할 HTML 태그의 종류를 나타냅니다.", + table: { + type: { summary: "ElementType" }, + }, + control: false, + }, + disabled: { + description: "텍스트 버튼이 비활성화되어 있는지 여부를 나타냅니다.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + control: { + type: "boolean", + }, + }, + size: { + description: "텍스트 버튼의 크기를 나타냅니다.", + table: { + type: { summary: "lg | sm" }, + defaultValue: { summary: "lg" }, + }, + control: { + type: "radio", + options: ["lg", "sm"], + }, + }, + onKeyDown: { + description: + "텍스트 버튼에 포커스 된 상태에서 엔터 키 또는 스페이스 바를 누르고 있는 동안 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + onKeyUp: { + description: + "텍스트 버튼에 포커스 된 상태에서 엔터 키 또는 스페이스 바를 뗐을 때 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + onMouseLeave: { + description: + "텍스트 버튼의 영역에서 마우스가 벗어났을 때 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + onPointerDown: { + description: + "텍스트 버튼에 포커스 된 상태에서 마우스 또는 터치로 누르고 있는 동안 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + onPointerUp: { + description: + "텍스트 버튼에 포커스 된 상태에서 마우스 또는 터치를 뗐을 때 동작할 이벤트를 나타냅니다.", + table: { + type: { summary: "() => void" }, + }, + control: false, + }, + style: { + description: "텍스트 버튼의 커스텀 스타일을 나타냅니다.", + table: { + type: { summary: "CSSProperties" }, + }, + control: false, + }, + className: { + description: "텍스트 버튼에 전달하는 커스텀 클래스를 나타냅니다.", + table: { + type: { summary: "string" }, + }, + control: false, + }, + ref: { + description: "렌더링된 요소 또는 컴포넌트에 연결할 ref를 나타냅니다.", + table: { + type: { summary: 'ComponentPropsWithRef["ref"]' }, + }, + control: false, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: { + text: "Button", + }, +}; + +export const Disabled: Story = { + args: { + text: "Button", + disabled: true, + }, +}; + +export const Large: Story = { + args: { + text: "Button", + }, +}; + +export const Small: Story = { + args: { + text: "Button", + size: "sm", + }, +}; diff --git a/packages/wow-ui/src/components/TextButton/index.tsx b/packages/wow-ui/src/components/TextButton/index.tsx new file mode 100644 index 00000000..30d1b19a --- /dev/null +++ b/packages/wow-ui/src/components/TextButton/index.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { css } from "@styled-system/css"; +import { styled } from "@styled-system/jsx"; +import type { CSSProperties, ElementType, ReactNode } from "react"; +import { forwardRef } from "react"; + +import useButton from "@/hooks/useButton"; +import type { + PolymorphicComponentProps, + PolymorphicComponentPropsWithRef, + PolymorphicRef, +} from "@/types"; + +/** + * @description 텍스트 버튼 컴포넌트의 속성을 정의합니다. + * + * @param {string} text - 텍스트 버튼의 라벨. + * @param {boolean} [disabled] - 텍스트 버튼이 비활성화되어 있는지 여부. + * @param {"lg" | "sm"} [size] - 텍스트 버튼의 크기. + * @param {() => void} [onKeyDown] - 텍스트 버튼에 포커스 된 상태에서 엔터 키 또는 스페이스 바를 누르고 있는 동안 동작할 이벤트. + * @param {() => void} [onMouseLeave] - 텍스트 버튼의 영역에서 마우스가 벗어났을 때 동작할 이벤트. + * @param {() => void} [onPointerDown] - 텍스트 버튼에 포커스 된 상태에서 마우스 또는 터치로 누르고 있는 동안 동작할 이벤트. + * @param {() => void} [onPointerUp] - 텍스트 버튼에 포커스 된 상태에서 마우스 또는 터치를 뗐을 때 동작할 이벤트. + * @param {CSSProperties} [style] - 텍스트 버튼의 커스텀 스타일. + * @param {string} [className] - 텍스트 버튼에 전달하는 커스텀 클래스. + * @param {ComponentPropsWithoutRef} rest 렌더링된 요소 또는 컴포넌트에 전달할 추가 props. + * @param {ComponentPropsWithRef["ref"]} ref 렌더링된 요소 또는 컴포넌트에 연결할 ref. + */ + +export interface CustomButtonProps { + text: string; + disabled?: boolean; + size?: "lg" | "sm"; + onKeyUp?: () => void; + onKeyDown?: () => void; + onMouseLeave?: () => void; + onPointerDown?: () => void; + onPointerUp?: () => void; + style?: CSSProperties; + className?: string; +} + +type ButtonProps = PolymorphicComponentProps< + C, + CustomButtonProps +>; + +type ButtonComponent = ( + props: PolymorphicComponentPropsWithRef> +) => ReactNode; + +const TextButton: ButtonComponent & { displayName?: string } = forwardRef( + ( + { + as, + text, + disabled = false, + size = "lg", + onKeyUp, + onKeyDown, + onMouseLeave, + onPointerDown, + onPointerUp, + ...rest + }: ButtonProps, + ref?: PolymorphicRef + ) => { + const Component = as || "button"; + + const { + pressed, + handleKeyDown, + handleKeyUp, + handlePointerDown, + handlePointerUp, + handleMouseLeave, + } = useButton({ + disabled, + onKeyUp, + onKeyDown, + onMouseLeave, + onPointerDown, + onPointerUp, + }); + + return ( + + + {text} + + + ); + } +); + +const TextButtonStyle = css({ + padding: "0.75rem 1.25rem", + + display: "flex", + alignItems: "center", + justifyContent: "center", + + color: "sub", + + borderRadius: "full", + cursor: "pointer", + + _hover: { + color: "textBlack", + }, + _pressed: { + background: "monoBackgroundPressed", + color: "sub", + }, + _disabled: { + color: "lightDisabled", + cursor: "not-allowed", + }, +}); + +TextButton.displayName = "TextButton"; +export default TextButton; diff --git a/packages/wow-ui/src/hooks/useButton.ts b/packages/wow-ui/src/hooks/useButton.ts new file mode 100644 index 00000000..c0b94c04 --- /dev/null +++ b/packages/wow-ui/src/hooks/useButton.ts @@ -0,0 +1,70 @@ +"use client"; + +import type { KeyboardEvent } from "react"; +import { useCallback, useState } from "react"; + +interface UseButtonProps { + disabled?: boolean; + onMouseLeave?: () => void; + onKeyUp?: () => void; + onKeyDown?: () => void; + onPointerDown?: () => void; + onPointerUp?: () => void; +} + +const useButton = ({ + disabled = false, + onMouseLeave, + onKeyUp, + onKeyDown, + onPointerDown, + onPointerUp, +}: UseButtonProps) => { + const [pressed, setPressed] = useState(false); + + const handleMouseLeave = useCallback(() => { + if (!disabled) setPressed(false); + onMouseLeave?.(); + }, [setPressed, disabled, onMouseLeave]); + + const handlePointerDown = useCallback(() => { + if (!disabled) setPressed(true); + onPointerDown?.(); + }, [setPressed, disabled, onPointerDown]); + + const handlePointerUp = useCallback(() => { + if (!disabled) setPressed(false); + onPointerUp?.(); + }, [setPressed, disabled, onPointerUp]); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === " " || event.key === "Enter") { + setPressed(true); + onKeyDown?.(); + } + }, + [setPressed, onKeyDown] + ); + + const handleKeyUp = useCallback( + (event: KeyboardEvent) => { + if (event.key === " " || event.key === "Enter") { + setPressed(false); + onKeyUp?.(); + } + }, + [setPressed, onKeyUp] + ); + + return { + pressed, + handleKeyDown, + handleKeyUp, + handleMouseLeave, + handlePointerDown, + handlePointerUp, + }; +}; + +export default useButton; diff --git a/packages/wow-ui/src/types/Polymorphic.ts b/packages/wow-ui/src/types/Polymorphic.ts index ae92de33..7193309d 100644 --- a/packages/wow-ui/src/types/Polymorphic.ts +++ b/packages/wow-ui/src/types/Polymorphic.ts @@ -11,11 +11,14 @@ export interface AsProps { export type PolymorphicRef = ComponentPropsWithRef["ref"]; +export type PolymorphicComponentPropsWithRef< + C extends ElementType, + Props = {}, +> = Props & { ref?: PolymorphicRef }; + export type PolymorphicComponentProps< T extends ElementType, Props = {}, > = AsProps & ComponentPropsWithoutRef & - Props & { - ref?: PolymorphicRef; - }; + PolymorphicComponentPropsWithRef;