Skip to content

Commit

Permalink
feat: help bubble (draft)
Browse files Browse the repository at this point in the history
  • Loading branch information
malangcat committed Dec 10, 2024
1 parent a437d4f commit 36e66bf
Show file tree
Hide file tree
Showing 27 changed files with 248 additions and 196 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
10 changes: 10 additions & 0 deletions docs/components/example/help-bubble-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ActionButton } from "@/registry/ui/action-button";
import { HelpBubbleTrigger } from "seed-design/ui/help-bubble";

export default function HelpBubblePreview() {
return (
<HelpBubbleTrigger title="타이틀" description="설명을 추가할 수 있어요.">
<ActionButton>열기</ActionButton>
</HelpBubbleTrigger>
);
}
1 change: 1 addition & 0 deletions docs/components/example/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"dismissible-inline-banner-with-icon-and-title-text": "import { IconILowercaseSerifCircleFill } from \"@daangn/react-monochrome-icon\";\nimport {\n DismissibleInlineBanner,\n InlineBannerDescription,\n InlineBannerTitle,\n} from \"seed-design/ui/inline-banner\";\n\nexport default function DismissibleInlineBannerWithIconAndTitleText() {\n return (\n <DismissibleInlineBanner\n dismissAriaLabel=\"닫기\"\n variant=\"informativeWeak\"\n icon={<IconILowercaseSerifCircleFill />}\n >\n <InlineBannerTitle>예약</InlineBannerTitle>\n <InlineBannerDescription>다른 사람과 예약된 물품이 있어요.</InlineBannerDescription>\n </DismissibleInlineBanner>\n );\n}",
"dismissible-inline-banner-with-icon": "import { DismissibleInlineBanner, InlineBannerDescription } from \"seed-design/ui/inline-banner\";\nimport { IconILowercaseSerifCircleFill } from \"@daangn/react-monochrome-icon\";\n\nexport default function DismissibleInlineBannerWithIcon() {\n return (\n <DismissibleInlineBanner\n dismissAriaLabel=\"닫기\"\n variant=\"informativeWeak\"\n icon={<IconILowercaseSerifCircleFill />}\n >\n <InlineBannerDescription>다른 사람과 예약된 물품이 있어요.</InlineBannerDescription>\n </DismissibleInlineBanner>\n );\n}",
"expand-button-preview": "import { ExpandButton } from \"seed-design/ui/expand-button\";\nimport { IconChevronRightFill } from \"@daangn/react-monochrome-icon\";\n\nexport default function ExpandButtonPreview() {\n return <ExpandButton suffixIcon={<IconChevronRightFill />}>라벨</ExpandButton>;\n}",
"help-bubble-preview": "import { ActionButton } from \"@/registry/ui/action-button\";\nimport { HelpBubbleTrigger } from \"seed-design/ui/help-bubble\";\n\nexport default function HelpBubblePreview() {\n return (\n <HelpBubbleTrigger title=\"타이틀\" description=\"설명을 추가할 수 있어요.\">\n <ActionButton>열기</ActionButton>\n </HelpBubbleTrigger>\n );\n}",
"inline-banner-activity": "import * as React from \"react\";\n\nimport {\n InlineBanner,\n InlineBannerDescription,\n type InlineBannerProps,\n} from \"seed-design/ui/inline-banner\";\nimport { ActionButton } from \"seed-design/ui/action-button\";\n\nimport type { ActivityComponentType } from \"@stackflow/react/future\";\nimport AppScreen from \"@/components/stackflow/ActivityLayout\";\n\ndeclare module \"@stackflow/config\" {\n interface Register {\n InlineBanner: unknown;\n }\n}\n\nconst InlineBannerActivity: ActivityComponentType<\"InlineBanner\"> = () => {\n const [variant, setVariant] =\n React.useState<Extract<InlineBannerProps[\"variant\"], \"neutralWeak\" | \"dangerSolid\">>(\n \"dangerSolid\",\n );\n\n return (\n <AppScreen>\n <InlineBanner\n variant={variant}\n style={variant === \"dangerSolid\" ? { position: \"sticky\", top: 0 } : undefined}\n >\n <InlineBannerDescription>\n Lorem ipsum dolor sit amet consectetur adipisicing elit.\n </InlineBannerDescription>\n </InlineBanner>\n <div style={{ display: \"flex\", flexDirection: \"column\", padding: \"1rem\", gap: \"0.75rem\" }}>\n <ActionButton\n onClick={() =>\n setVariant((prev) => (prev === \"dangerSolid\" ? \"neutralWeak\" : \"dangerSolid\"))\n }\n >\n Toggle tone\n </ActionButton>\n <p style={{ marginBlock: 0, lineHeight: 1.35 }}>\n Lorem ipsum dolor sit, amet consectetur adipisicing elit. At a eaque fugiat sint sapiente.\n Id, hic ex, blanditiis totam animi amet delectus temporibus quae fugiat magnam, quos eaque\n dolorum a? Lorem ipsum dolor, sit amet consectetur adipisicing elit. Possimus labore unde\n minus temporibus beatae commodi et nesciunt iure in dignissimos suscipit, alias ab\n voluptatem facilis tempora numquam. Veritatis, dolorum suscipit! Lorem ipsum dolor sit,\n amet consectetur adipisicing elit. Explicabo fugiat molestias iusto, ipsum distinctio\n officia ad id ratione esse ducimus architecto deleniti illum reiciendis rerum, at\n blanditiis molestiae. Cupiditate, nobis? Lorem ipsum dolor sit amet consectetur\n adipisicing elit. Ab, magni. Aliquid inventore quaerat nemo architecto harum earum quas\n porro repudiandae explicabo repellat repellendus magni, corporis omnis laborum, velit\n dicta blanditiis. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Debitis,\n eveniet quas. Accusamus facere veritatis expedita delectus, asperiores numquam placeat\n necessitatibus assumenda, nesciunt in dolorem sit provident repellendus, voluptatem earum!\n Consequatur. Lorem ipsum dolor, sit amet consectetur adipisicing elit. Aut earum\n asperiores aliquam magnam est delectus veritatis numquam sint porro tenetur dolores nobis,\n deleniti voluptas quaerat, quia voluptatum soluta autem perspiciatis? Lorem ipsum dolor\n sit amet consectetur adipisicing elit. Facilis possimus eaque aliquam maxime? Quidem enim,\n sed itaque at veritatis nihil officia esse qui provident ipsa adipisci necessitatibus\n officiis distinctio laborum!\n </p>\n </div>\n </AppScreen>\n );\n};\n\nexport default InlineBannerActivity;",
"inline-banner-danger-solid": "import { InlineBanner, InlineBannerDescription } from \"seed-design/ui/inline-banner\";\nimport { IconExclamationmarkCircleFill } from \"@daangn/react-monochrome-icon\";\n\nexport default function InlineBannerDangerSolid() {\n return (\n <InlineBanner variant=\"dangerSolid\" icon={<IconExclamationmarkCircleFill />}>\n <InlineBannerDescription>사업자 정보를 등록해주세요.</InlineBannerDescription>\n </InlineBanner>\n );\n}",
"inline-banner-danger-weak": "import { InlineBanner, InlineBannerDescription } from \"seed-design/ui/inline-banner\";\nimport { IconExclamationmarkCircleFill } from \"@daangn/react-monochrome-icon\";\n\nexport default function InlineBannerDangerWeak() {\n return (\n <InlineBanner variant=\"dangerWeak\" icon={<IconExclamationmarkCircleFill />}>\n <InlineBannerDescription>사업자 정보를 등록해주세요.</InlineBannerDescription>\n </InlineBanner>\n );\n}",
Expand Down
11 changes: 11 additions & 0 deletions docs/content/docs/react/components/help-bubble.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Help Bubble
---

<ComponentExample name="help-bubble-preview" />

### 설치

<Installation name="help-bubble" />

## 예제
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@seed-design/react-checkbox": "0.0.0-alpha-20241030023710",
"@seed-design/react-dismissible": "0.0.0",
"@seed-design/react-icon": "^0.7.3",
"@seed-design/react-popover": "0.0.0-alpha-20241030023710",
"@seed-design/react-progress": "0.0.0",
"@seed-design/react-radio-group": "0.0.0-alpha-20241030023710",
"@seed-design/react-segmented-control": "workspace:^",
Expand Down
14 changes: 14 additions & 0 deletions docs/public/__registry__/ui/help-bubble.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "help-bubble",
"dependencies": [
"@seed-design/react-popover",
"@radix-ui/react-slot"
],
"registries": [
{
"name": "help-bubble.tsx",
"type": "ui",
"content": "\"use client\";\n\nimport \"@seed-design/stylesheet/helpBubble.css\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { usePopover, type UsePopoverProps } from \"@seed-design/react-popover\";\nimport { helpBubble } from \"@seed-design/recipe/helpBubble\";\nimport { createContext, forwardRef, useContext } from \"react\";\nimport { mergeRefs } from \"../util/mergeRefs\";\n\ninterface HelpBubbleArrowProps extends React.ComponentPropsWithRef<\"svg\"> {\n width: number;\n\n height: number;\n\n tipRadius: number;\n}\n\nconst HelpBubbleArrow = forwardRef<SVGSVGElement, HelpBubbleArrowProps>(\n (props, ref) => {\n const { width, height, tipRadius, ...otherProps } = props;\n const pathData = `M0,0\n H${width}\n L${width / 2 + tipRadius},${height - tipRadius}\n Q${width / 2},${height} ${width / 2 - tipRadius},${height - tipRadius}\n Z`;\n\n return (\n <svg\n aria-hidden=\"true\"\n width={width}\n height={width}\n viewBox={`0 0 ${width} ${height > width ? height : width}`}\n ref={ref}\n {...otherProps}\n >\n <path stroke=\"none\" d={pathData} />\n </svg>\n );\n },\n);\n\nconst HelpBubbleContext = createContext<{\n api: ReturnType<typeof usePopover>;\n} | null>(null);\n\nexport interface HelpBubbleTriggerProps extends UsePopoverProps {\n title: string;\n\n description?: string;\n\n children?: React.ReactNode;\n}\n\nexport const HelpBubbleTrigger = forwardRef<\n HTMLButtonElement,\n HelpBubbleTriggerProps\n>((props, ref) => {\n const {\n open,\n defaultOpen,\n onOpenChange,\n placement = \"top\",\n gutter = 4,\n overflowPadding = 16,\n arrowPadding = 14,\n flip,\n slide,\n strategy,\n title,\n description,\n ...otherProps\n } = props;\n\n const api = usePopover({\n open,\n defaultOpen,\n onOpenChange,\n placement,\n gutter,\n overflowPadding,\n arrowPadding,\n flip,\n slide,\n strategy,\n });\n const classNames = helpBubble();\n\n const arrowRect = api.rects.arrow;\n\n return (\n <>\n <Slot\n ref={mergeRefs(ref, api.refs.trigger)}\n {...api.triggerProps}\n {...otherProps}\n />\n {api.open && (\n <div\n ref={api.refs.positioner}\n {...api.positionerProps}\n className={classNames.positioner}\n >\n <div className={classNames.content}>\n <div\n ref={api.refs.arrow}\n {...api.arrowProps}\n className={classNames.arrow}\n >\n <HelpBubbleArrow\n width={arrowRect?.width ?? 0}\n height={arrowRect?.height ?? 0}\n tipRadius={1}\n />\n </div>\n <span className={classNames.title}>{props.title}</span>\n {props.description && (\n <span className={classNames.description}>\n {props.description}\n </span>\n )}\n </div>\n </div>\n )}\n </>\n );\n});\n"
}
]
}
10 changes: 10 additions & 0 deletions docs/public/__registry__/ui/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@
"ui:inline-banner.tsx"
]
},
{
"name": "help-bubble",
"dependencies": [
"@seed-design/react-popover",
"@radix-ui/react-slot"
],
"files": [
"ui:help-bubble.tsx"
]
},
{
"name": "tabs",
"dependencies": [
Expand Down
8 changes: 4 additions & 4 deletions docs/public/rootage/components/help-bubble.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"base": {
"enabled": {
"root": {
"cornerRadius": "$radius.x1_5",
"cornerRadius": "$radius.x3",
"paddingX": "$unit.x3",
"paddingY": "$unit.x2"
"paddingY": "$unit.x3"
},
"arrow": {
"size": "$unit.x2_5"
Expand All @@ -28,10 +28,10 @@
"variant=non-modal": {
"enabled": {
"root": {
"color": "$color.bg.neutral-solid"
"color": "$color.bg.floating-solid"
},
"arrow": {
"color": "$color.bg.neutral-solid"
"color": "$color.bg.floating-solid"
},
"title": {
"color": "$color.fg.neutral-inverted"
Expand Down
5 changes: 5 additions & 0 deletions docs/registry/registry-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export const registryUI: RegistryUI = [
],
files: ["ui:inline-banner.tsx"],
},
{
name: "help-bubble",
dependencies: ["@seed-design/react-popover", "@radix-ui/react-slot"],
files: ["ui:help-bubble.tsx"],
},
{
name: "tabs",
// TODO: remove alpha
Expand Down
127 changes: 127 additions & 0 deletions docs/registry/ui/help-bubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"use client";

import "@seed-design/stylesheet/helpBubble.css";

import { Slot } from "@radix-ui/react-slot";
import { usePopover, type UsePopoverProps } from "@seed-design/react-popover";
import { helpBubble } from "@seed-design/recipe/helpBubble";
import { createContext, forwardRef, useContext } from "react";
import { mergeRefs } from "../util/mergeRefs";

interface HelpBubbleArrowProps extends React.ComponentPropsWithRef<"svg"> {
width: number;

height: number;

tipRadius: number;
}

const HelpBubbleArrow = forwardRef<SVGSVGElement, HelpBubbleArrowProps>(
(props, ref) => {
const { width, height, tipRadius, ...otherProps } = props;
const pathData = `M0,0
H${width}
L${width / 2 + tipRadius},${height - tipRadius}
Q${width / 2},${height} ${width / 2 - tipRadius},${height - tipRadius}
Z`;

return (
<svg
aria-hidden="true"
width={width}
height={width}
viewBox={`0 0 ${width} ${height > width ? height : width}`}
ref={ref}
{...otherProps}
>
<path stroke="none" d={pathData} />
</svg>
);
},
);

const HelpBubbleContext = createContext<{
api: ReturnType<typeof usePopover>;
} | null>(null);

export interface HelpBubbleTriggerProps extends UsePopoverProps {
title: string;

description?: string;

children?: React.ReactNode;
}

export const HelpBubbleTrigger = forwardRef<
HTMLButtonElement,
HelpBubbleTriggerProps
>((props, ref) => {
const {
open,
defaultOpen,
onOpenChange,
placement = "top",
gutter = 4,
overflowPadding = 16,
arrowPadding = 14,
flip,
slide,
strategy,
title,
description,
...otherProps
} = props;

const api = usePopover({
open,
defaultOpen,
onOpenChange,
placement,
gutter,
overflowPadding,
arrowPadding,
flip,
slide,
strategy,
});
const classNames = helpBubble();

const arrowRect = api.rects.arrow;

return (
<>
<Slot
ref={mergeRefs(ref, api.refs.trigger)}
{...api.triggerProps}
{...otherProps}
/>
{api.open && (
<div
ref={api.refs.positioner}
{...api.positionerProps}
className={classNames.positioner}
>
<div className={classNames.content}>
<div
ref={api.refs.arrow}
{...api.arrowProps}
className={classNames.arrow}
>
<HelpBubbleArrow
width={arrowRect?.width ?? 0}
height={arrowRect?.height ?? 0}
tipRadius={1}
/>
</div>
<span className={classNames.title}>{props.title}</span>
{props.description && (
<span className={classNames.description}>
{props.description}
</span>
)}
</div>
</div>
)}
</>
);
});
19 changes: 19 additions & 0 deletions docs/registry/util/mergeRefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type * as React from "react";

export function mergeRefs<T>(
...refs: React.ForwardedRef<T>[]
): React.ForwardedRef<T> {
if (refs.length === 1) {
return refs[0];
}

return (value: T | null) => {
for (const ref of refs) {
if (typeof ref === "function") {
ref(value);
} else if (ref != null) {
ref.current = value;
}
}
};
}
8 changes: 0 additions & 8 deletions packages/react-headless/popover/.tshy/build.json

This file was deleted.

16 changes: 0 additions & 16 deletions packages/react-headless/popover/.tshy/commonjs.json

This file was deleted.

15 changes: 0 additions & 15 deletions packages/react-headless/popover/.tshy/esm.json

This file was deleted.

34 changes: 9 additions & 25 deletions packages/react-headless/popover/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,28 @@
},
"sideEffects": false,
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"source": "./src/index.ts",
"default": "./dist/esm/index.ts"
},
"require": {
"source": "./src/index.ts",
"default": "./dist/commonjs/index.ts"
}
"types": "./lib/index.d.ts",
"require": "./lib/index.js",
"import": "./lib/index.mjs"
}
},
"main": "./lib/index.js",
"files": [
"dist",
"lib",
"src"
],
"scripts": {
"clean": "rm -rf dist",
"prepack": "yarn build",
"build": "tshy"
"clean": "rm -rf lib",
"build": "nanobundle build"
},
"dependencies": {
"@floating-ui/react": "^0.26.19",
"@seed-design/dom-utils": "0.0.0-alpha-20241030023710"
},
"devDependencies": {
"tshy": "^1.17.0",
"typescript": "^5.4.5"
"nanobundle": "^1.6.0"
},
"peerDependencies": {
"react": ">=18.0.0",
Expand All @@ -44,20 +38,10 @@
"publishConfig": {
"access": "public"
},
"tshy": {
"liveDev": true,
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts"
}
},
"ultra": {
"concurrent": [
"dev",
"build"
]
},
"type": "module",
"module": "./dist/esm/index.ts",
"main": "./dist/commonjs/index.ts"
}
}
2 changes: 1 addition & 1 deletion packages/react-headless/popover/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useClick, useDismiss, useInteractions, useRole } from "@floating-ui/react";
import { usePositionedFloating, type UsePositionedFloatingProps } from "./floating.js";
import { usePositionedFloating, type UsePositionedFloatingProps } from "./floating";

// TODO: useRole이 임의로 id를 생성하는 문제가 있음. 동작만 참고하고 role="dialog"에 맞게 aria attribute 설정을 직접 해야 함.

Expand Down
Loading

0 comments on commit 36e66bf

Please sign in to comment.