From c26db7fbacf3353ae3c62fbbc13c7c205a530591 Mon Sep 17 00:00:00 2001 From: Joo Chanhwi <56245920+te6-in@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:13:12 +0900 Subject: [PATCH] refactor: make `useDismissible` private & export `DismissibleRoot` and`DismissibleDismissButton` --- docs/registry/registry-ui.ts | 4 +- docs/registry/ui/callout.tsx | 56 ++++++------ docs/registry/ui/inline-banner.tsx | 80 ++++++++--------- packages/react/package.json | 1 + .../react/src/components/Callout/Callout.tsx | 40 +++------ .../components/InlineBanner/InlineBanner.tsx | 39 +++------ .../src/components/private/useDismissible.tsx | 86 +++++++++++++++++++ packages/react/src/hooks/index.ts | 1 - packages/react/src/hooks/useDismissible.tsx | 52 ----------- packages/react/src/index.tsx | 1 - 10 files changed, 169 insertions(+), 191 deletions(-) create mode 100644 packages/react/src/components/private/useDismissible.tsx delete mode 100644 packages/react/src/hooks/index.ts delete mode 100644 packages/react/src/hooks/useDismissible.tsx diff --git a/docs/registry/registry-ui.ts b/docs/registry/registry-ui.ts index 6d8ee1a51..93ca13a14 100644 --- a/docs/registry/registry-ui.ts +++ b/docs/registry/registry-ui.ts @@ -128,9 +128,7 @@ export const registryUI: RegistryUI = [ }, { name: "segmented-control", - dependencies: [ - `@seed-design/react-segmented-control@${segmentedControlPkg.version}`, - ], + dependencies: ["@seed-design/react"], files: ["ui:segmented-control.tsx"], }, { diff --git a/docs/registry/ui/callout.tsx b/docs/registry/ui/callout.tsx index 9274ed5aa..00847431b 100644 --- a/docs/registry/ui/callout.tsx +++ b/docs/registry/ui/callout.tsx @@ -2,12 +2,7 @@ import "@seed-design/stylesheet/callout.css"; -import { - Callout as SeedCallout, - DismissibleProvider, - useDismissible, - type UseDismissibleProps, -} from "@seed-design/react"; +import { Callout as SeedCallout } from "@seed-design/react"; import * as React from "react"; import { @@ -16,7 +11,10 @@ import { } from "@daangn/react-monochrome-icon"; export interface CalloutProps - extends Omit { + extends Omit< + SeedCallout.RootProps, + "children" | "title" | "asChild" | "open" | "defaultOpen" | "onDismiss" + > { icon?: React.ReactNode; title?: React.ReactNode; description: React.ReactNode; @@ -49,7 +47,10 @@ export const Callout = React.forwardRef< Callout.displayName = "Callout"; export interface ActionableCalloutProps - extends Omit { + extends Omit< + SeedCallout.RootProps, + "children" | "title" | "asChild" | "open" | "defaultOpen" | "onDismiss" + > { title?: React.ReactNode; description: React.ReactNode; } @@ -78,8 +79,7 @@ export const ActionableCallout = React.forwardRef< ActionableCallout.displayName = "ActionableCallout"; export interface DismissibleCalloutProps - extends Omit, - UseDismissibleProps { + extends Omit { title?: React.ReactNode; description: React.ReactNode; linkLabel?: React.ReactNode; @@ -106,28 +106,22 @@ export const DismissibleCallout = React.forwardRef< }, ref, ) => { - const api = useDismissible({ defaultOpen, open, onDismiss }); - - if (!api.open) return null; - return ( - - - - {title} - {description && ( - {description} - )} - {linkLabel && ( - {linkLabel} - )} - - {/* You may implement your own i18n for dismiss label */} - - } /> - - - + + + {title} + {description && ( + {description} + )} + {linkLabel && ( + {linkLabel} + )} + + {/* You may implement your own i18n for dismiss label */} + + } /> + + ); }, ); diff --git a/docs/registry/ui/inline-banner.tsx b/docs/registry/ui/inline-banner.tsx index 4228ac1ba..feeb56193 100644 --- a/docs/registry/ui/inline-banner.tsx +++ b/docs/registry/ui/inline-banner.tsx @@ -2,12 +2,7 @@ import "@seed-design/stylesheet/inlineBanner.css"; -import { - InlineBanner as SeedInlineBanner, - DismissibleProvider, - useDismissible, - type UseDismissibleProps, -} from "@seed-design/react"; +import { InlineBanner as SeedInlineBanner } from "@seed-design/react"; import * as React from "react"; import { @@ -16,7 +11,10 @@ import { } from "@daangn/react-monochrome-icon"; export interface InlineBannerProps - extends Omit { + extends Omit< + SeedInlineBanner.RootProps, + "children" | "title" | "asChild" | "open" | "defaultOpen" | "onDismiss" + > { icon?: React.ReactNode; title?: React.ReactNode; description: React.ReactNode; @@ -46,7 +44,10 @@ export const InlineBanner = React.forwardRef< InlineBanner.displayName = "InlineBanner"; export interface LinkInlineBannerProps - extends Omit { + extends Omit< + SeedInlineBanner.RootProps, + "children" | "title" | "asChild" | "open" | "defaultOpen" | "onDismiss" + > { icon?: React.ReactNode; title?: React.ReactNode; description: React.ReactNode; @@ -76,7 +77,10 @@ export const LinkInlineBanner = React.forwardRef< LinkInlineBanner.displayName = "LinkInlineBanner"; export interface ActionableInlineBannerProps - extends Omit { + extends Omit< + SeedInlineBanner.RootProps, + "children" | "title" | "asChild" | "open" | "defaultOpen" | "onDismiss" + > { icon?: React.ReactNode; title?: React.ReactNode; description: React.ReactNode; @@ -110,10 +114,9 @@ ActionableInlineBanner.displayName = "ActionableInlineBanner"; export interface DismissibleInlineBannerProps extends Omit< - SeedInlineBanner.RootProps, - "variant" | "children" | "title" | "asChild" - >, - UseDismissibleProps { + SeedInlineBanner.RootProps, + "variant" | "children" | "title" | "asChild" + > { icon?: React.ReactNode; title?: React.ReactNode; description: React.ReactNode; @@ -129,36 +132,23 @@ export interface DismissibleInlineBannerProps export const DismissibleInlineBanner = React.forwardRef< React.ElementRef, DismissibleInlineBannerProps ->( - ( - { icon, title, description, defaultOpen, open, onDismiss, ...otherProps }, - ref, - ) => { - const api = useDismissible({ defaultOpen, open, onDismiss }); - - if (!api.open) return null; - - return ( - - - - {icon && } - - {title && ( - {title} - )} - - {description} - - - - {/* You may implement your own i18n for dismiss label */} - - } /> - - - - ); - }, -); +>(({ icon, title, description, ...otherProps }, ref) => { + return ( + + + {icon && } + + {title && {title}} + + {description} + + + + {/* You may implement your own i18n for dismiss label */} + + } /> + + + ); +}); DismissibleInlineBanner.displayName = "DismissibleInlineBanner"; diff --git a/packages/react/package.json b/packages/react/package.json index 597b2ba52..9443a3052 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-use-controllable-state": "1.0.1", "@radix-ui/react-use-layout-effect": "^1.0.1", "@seed-design/dom-utils": "0.0.0-alpha-20241030023710", "@seed-design/react-avatar": "0.0.0-alpha-20241030023710", diff --git a/packages/react/src/components/Callout/Callout.tsx b/packages/react/src/components/Callout/Callout.tsx index 12b800795..d1056c8d8 100644 --- a/packages/react/src/components/Callout/Callout.tsx +++ b/packages/react/src/components/Callout/Callout.tsx @@ -5,16 +5,17 @@ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive"; import { callout, type CalloutVariantProps } from "@seed-design/recipe/callout"; import { createStyleContext } from "../../utils/createStyleContext"; import { Icon, type IconProps } from "../private/Icon"; -import { useDismissibleContext } from "../../hooks/useDismissible"; +import { + DismissibleDismissButton, + DismissibleRoot, + type DismissibleRootProps, +} from "../private/useDismissible"; const { withContext, withProvider, useClassNames } = createStyleContext(callout); -export interface CalloutRootProps - extends CalloutVariantProps, - PrimitiveProps, - React.HTMLAttributes {} +export interface CalloutRootProps extends CalloutVariantProps, DismissibleRootProps {} -export const CalloutRoot = withProvider(Primitive.div, "root"); +export const CalloutRoot = withProvider(DismissibleRoot, "root"); export interface CalloutIconProps extends IconProps {} @@ -29,6 +30,7 @@ export const CalloutTextContent = React.forwardRef; }, ); +CalloutTextContent.displayName = "CalloutTextContent"; export interface CalloutTitleProps extends PrimitiveProps, React.HTMLAttributes {} @@ -81,30 +83,10 @@ export interface CalloutDismissButtonProps extends PrimitiveProps, React.ButtonHTMLAttributes {} -export const CalloutDismissButton = React.forwardRef( - ({ className, onClick, ...otherProps }, ref) => { - const classNames = useClassNames(); - const { handleDismiss } = useDismissibleContext(); - - const handleClick: React.MouseEventHandler = React.useCallback( - (event) => { - onClick?.(event); - handleDismiss(); - }, - [handleDismiss, onClick], - ); - - return ( - - ); - }, +export const CalloutDismissButton = withContext( + DismissibleDismissButton, + "dismissButton", ); -CalloutDismissButton.displayName = "CalloutDismissButton"; export interface CalloutDismissIconProps extends IconProps {} diff --git a/packages/react/src/components/InlineBanner/InlineBanner.tsx b/packages/react/src/components/InlineBanner/InlineBanner.tsx index 23c1d992d..d95f5083f 100644 --- a/packages/react/src/components/InlineBanner/InlineBanner.tsx +++ b/packages/react/src/components/InlineBanner/InlineBanner.tsx @@ -5,17 +5,18 @@ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive"; import { inlineBanner, type InlineBannerVariantProps } from "@seed-design/recipe/inlineBanner"; import { createStyleContext } from "../../utils/createStyleContext"; import { Icon, type IconProps } from "../private/Icon"; -import { useDismissibleContext } from "../../hooks/useDismissible"; +import { + DismissibleDismissButton, + DismissibleRoot, + type DismissibleRootProps, +} from "../private/useDismissible"; const { withContext, withProvider, useClassNames } = createStyleContext(inlineBanner); -export interface InlineBannerRootProps - extends InlineBannerVariantProps, - PrimitiveProps, - React.HTMLAttributes {} +export interface InlineBannerRootProps extends InlineBannerVariantProps, DismissibleRootProps {} export const InlineBannerRoot = withProvider( - Primitive.div, + DismissibleRoot, "root", ); @@ -42,6 +43,7 @@ export const InlineBannerTextContent = React.forwardRef< >((props, ref) => { return ; }); +InlineBannerTextContent.displayName = "InlineBannerTextContent"; export interface InlineBannerTitleProps extends PrimitiveProps, @@ -83,31 +85,10 @@ export interface InlineBannerDismissButtonProps extends PrimitiveProps, React.ButtonHTMLAttributes {} -export const InlineBannerDismissButton = React.forwardRef< +export const InlineBannerDismissButton = withContext< HTMLButtonElement, InlineBannerDismissButtonProps ->(({ className, onClick, ...otherProps }, ref) => { - const classNames = useClassNames(); - const { handleDismiss } = useDismissibleContext(); - - const handleClick: React.MouseEventHandler = React.useCallback( - (event) => { - onClick?.(event); - handleDismiss(); - }, - [handleDismiss, onClick], - ); - - return ( - - ); -}); -InlineBannerDismissButton.displayName = "InlineBannerDismissButton"; +>(DismissibleDismissButton, "dismissButton"); export interface InlineBannerDismissIconProps extends IconProps {} diff --git a/packages/react/src/components/private/useDismissible.tsx b/packages/react/src/components/private/useDismissible.tsx new file mode 100644 index 000000000..55a4d8ea5 --- /dev/null +++ b/packages/react/src/components/private/useDismissible.tsx @@ -0,0 +1,86 @@ +import { useControllableState } from "@radix-ui/react-use-controllable-state"; +import { buttonProps, elementProps, mergeProps } from "@seed-design/dom-utils"; +import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive"; +import * as React from "react"; + +export interface UseDismissibleProps { + defaultOpen?: boolean; + open?: boolean; + onDismiss?: () => void; +} + +export type UseDismissibleReturn = ReturnType; + +export function useDismissible(props: UseDismissibleProps) { + const [open = true, setOpen] = useControllableState({ + prop: props.open, + defaultProp: props.defaultOpen, + onChange: (open) => { + if (!open) { + props.onDismiss?.(); + } + }, + }); + + const dismiss = React.useCallback(() => setOpen(false), [setOpen]); + + return { + open, + dismiss, + + rootProps: elementProps({}), + + dismissButtonProps: buttonProps({ + onClick: (e) => { + if (e.defaultPrevented) return; + + dismiss(); + }, + }), + }; +} + +const DismissibleContext = React.createContext | null>(null); + +export const DismissibleProvider = DismissibleContext.Provider; + +export const useDismissibleContext = () => { + const context = React.useContext(DismissibleContext); + if (context === null) { + throw new Error("useDismissibleContext should be used within DismissibleProvider"); + } + + return context; +}; + +export interface DismissibleRootProps + extends PrimitiveProps, + UseDismissibleProps, + React.HTMLAttributes {} + +export const DismissibleRoot = React.forwardRef( + ({ defaultOpen, open, onDismiss, ...otherProps }, ref) => { + const api = useDismissible({ defaultOpen, open, onDismiss }); + + if (!api.open) return null; + + return ( + + + + ); + }, +); + +export interface DismissibleDismissButtonProps + extends PrimitiveProps, + React.HTMLAttributes {} + +export const DismissibleDismissButton = React.forwardRef< + HTMLButtonElement, + DismissibleDismissButtonProps +>((props, ref) => { + const { dismissButtonProps } = useDismissibleContext(); + + return ; +}); diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts deleted file mode 100644 index 94c20769e..000000000 --- a/packages/react/src/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useDismissible"; diff --git a/packages/react/src/hooks/useDismissible.tsx b/packages/react/src/hooks/useDismissible.tsx deleted file mode 100644 index 5dc7bad81..000000000 --- a/packages/react/src/hooks/useDismissible.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react"; - -export interface UseDismissibleProps { - /** - * @default true - */ - defaultOpen?: boolean; - open?: boolean; - onDismiss?: () => void; -} - -export type UseDismissibleReturn = ReturnType; - -export function useDismissible({ - defaultOpen = true, - open: propOpen, - onDismiss, -}: UseDismissibleProps) { - const [stateOpen, setStateOpen] = React.useState(propOpen ?? defaultOpen); - - function handleDismiss() { - onDismiss?.(); - - if (propOpen === undefined) { - setStateOpen(false); - } - } - - React.useEffect(() => { - if (propOpen !== undefined && propOpen !== stateOpen) { - setStateOpen(propOpen); - } - }, [propOpen, stateOpen]); - - return { - open: stateOpen, - handleDismiss, - }; -} - -const DismissibleContext = React.createContext | null>(null); - -export const DismissibleProvider = DismissibleContext.Provider; - -export const useDismissibleContext = () => { - const context = React.useContext(DismissibleContext); - if (context === null) { - throw new Error("useDismissibleContext should be used within DismissibleProvider"); - } - - return context; -}; diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index a234113be..40b494c5f 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -1,2 +1 @@ export * from "./components"; -export * from "./hooks";