@seed-design/react 2.0.5 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/lib/components/ContentDialog/ContentDialog.cjs +105 -0
  2. package/lib/components/ContentDialog/ContentDialog.d.ts +50 -0
  3. package/lib/components/ContentDialog/ContentDialog.d.ts.map +1 -0
  4. package/lib/components/ContentDialog/ContentDialog.js +91 -0
  5. package/lib/components/ContentDialog/ContentDialog.namespace.cjs +24 -0
  6. package/lib/components/ContentDialog/ContentDialog.namespace.d.ts +2 -0
  7. package/lib/components/ContentDialog/ContentDialog.namespace.d.ts.map +1 -0
  8. package/lib/components/ContentDialog/ContentDialog.namespace.js +19 -0
  9. package/lib/components/ContentDialog/index.cjs +21 -0
  10. package/lib/components/ContentDialog/index.d.ts +3 -0
  11. package/lib/components/ContentDialog/index.d.ts.map +1 -0
  12. package/lib/components/ContentDialog/index.js +3 -0
  13. package/lib/components/Dialog/index.d.ts.map +1 -1
  14. package/lib/components/QuantityPicker/QuantityPicker.cjs +120 -0
  15. package/lib/components/QuantityPicker/QuantityPicker.d.ts +25 -0
  16. package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -0
  17. package/lib/components/QuantityPicker/QuantityPicker.js +113 -0
  18. package/lib/components/QuantityPicker/QuantityPicker.namespace.cjs +17 -0
  19. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts +2 -0
  20. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts.map +1 -0
  21. package/lib/components/QuantityPicker/QuantityPicker.namespace.js +12 -0
  22. package/lib/components/QuantityPicker/QuantityPicker.test.d.ts +2 -0
  23. package/lib/components/QuantityPicker/QuantityPicker.test.d.ts.map +1 -0
  24. package/lib/components/QuantityPicker/index.cjs +14 -0
  25. package/lib/components/QuantityPicker/index.d.ts +3 -0
  26. package/lib/components/QuantityPicker/index.d.ts.map +1 -0
  27. package/lib/components/QuantityPicker/index.js +3 -0
  28. package/lib/components/ResponsiveDialog/ResponsiveDialog.cjs +196 -0
  29. package/lib/components/ResponsiveDialog/ResponsiveDialog.d.ts +72 -0
  30. package/lib/components/ResponsiveDialog/ResponsiveDialog.d.ts.map +1 -0
  31. package/lib/components/ResponsiveDialog/ResponsiveDialog.js +181 -0
  32. package/lib/components/ResponsiveDialog/ResponsiveDialog.namespace.cjs +25 -0
  33. package/lib/components/ResponsiveDialog/ResponsiveDialog.namespace.d.ts +2 -0
  34. package/lib/components/ResponsiveDialog/ResponsiveDialog.namespace.d.ts.map +1 -0
  35. package/lib/components/ResponsiveDialog/ResponsiveDialog.namespace.js +20 -0
  36. package/lib/components/ResponsiveDialog/index.cjs +23 -0
  37. package/lib/components/ResponsiveDialog/index.d.ts +3 -0
  38. package/lib/components/ResponsiveDialog/index.d.ts.map +1 -0
  39. package/lib/components/ResponsiveDialog/index.js +3 -0
  40. package/lib/components/Select/Select.cjs +191 -0
  41. package/lib/components/Select/Select.d.ts +89 -0
  42. package/lib/components/Select/Select.d.ts.map +1 -0
  43. package/lib/components/Select/Select.js +171 -0
  44. package/lib/components/Select/Select.namespace.cjs +30 -0
  45. package/lib/components/Select/Select.namespace.d.ts +2 -0
  46. package/lib/components/Select/Select.namespace.d.ts.map +1 -0
  47. package/lib/components/Select/Select.namespace.js +25 -0
  48. package/lib/components/Select/Select.test.d.ts +2 -0
  49. package/lib/components/Select/Select.test.d.ts.map +1 -0
  50. package/lib/components/Select/index.cjs +27 -0
  51. package/lib/components/Select/index.d.ts +3 -0
  52. package/lib/components/Select/index.d.ts.map +1 -0
  53. package/lib/components/Select/index.js +3 -0
  54. package/lib/components/index.cjs +81 -0
  55. package/lib/components/index.d.ts +4 -0
  56. package/lib/components/index.d.ts.map +1 -1
  57. package/lib/components/index.js +9 -1
  58. package/lib/index.cjs +82 -1
  59. package/lib/index.js +10 -2
  60. package/package.json +8 -6
  61. package/src/components/ContentDialog/ContentDialog.namespace.ts +26 -0
  62. package/src/components/ContentDialog/ContentDialog.tsx +222 -0
  63. package/src/components/ContentDialog/index.ts +33 -0
  64. package/src/components/Dialog/index.ts +5 -0
  65. package/src/components/QuantityPicker/QuantityPicker.namespace.ts +12 -0
  66. package/src/components/QuantityPicker/QuantityPicker.test.tsx +108 -0
  67. package/src/components/QuantityPicker/QuantityPicker.tsx +214 -0
  68. package/src/components/QuantityPicker/index.ts +14 -0
  69. package/src/components/ResponsiveDialog/ResponsiveDialog.namespace.ts +28 -0
  70. package/src/components/ResponsiveDialog/ResponsiveDialog.tsx +327 -0
  71. package/src/components/ResponsiveDialog/index.ts +31 -0
  72. package/src/components/Select/Select.namespace.ts +38 -0
  73. package/src/components/Select/Select.test.tsx +451 -0
  74. package/src/components/Select/Select.tsx +443 -0
  75. package/src/components/Select/index.ts +40 -0
  76. package/src/components/index.ts +4 -0
@@ -0,0 +1,222 @@
1
+ import { composeRefs } from "@radix-ui/react-compose-refs";
2
+ import {
3
+ contentDialog,
4
+ type ContentDialogVariantProps,
5
+ } from "@seed-design/css/recipes/content-dialog";
6
+ import { dataAttr } from "@seed-design/dom-utils";
7
+ import { Dialog as DialogPrimitive } from "@seed-design/react-dialog";
8
+ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
9
+ import clsx from "clsx";
10
+ import * as React from "react";
11
+ import { createRenderTrackingContext } from "../../utils/createRenderTrackingContext";
12
+ import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
13
+ import { useStyleProps, withStyleProps, type StyleProps } from "../../utils/styled";
14
+
15
+ const { withContext, useClassNames, ClassNamesProvider } = createSlotRecipeContext(contentDialog);
16
+
17
+ const closeButtonTracker = createRenderTrackingContext("ContentDialogCloseButton");
18
+
19
+ ////////////////////////////////////////////////////////////////////////////////////
20
+
21
+ export interface ContentDialogRootProps
22
+ extends ContentDialogVariantProps,
23
+ Omit<DialogPrimitive.RootProps, "role"> {
24
+ /**
25
+ * @default true
26
+ */
27
+ lazyMount?: DialogPrimitive.RootProps["lazyMount"];
28
+ /**
29
+ * @default true
30
+ */
31
+ unmountOnExit?: DialogPrimitive.RootProps["unmountOnExit"];
32
+ }
33
+
34
+ export function ContentDialogRoot(props: ContentDialogRootProps) {
35
+ const [variantProps, otherProps] = contentDialog.splitVariantProps({
36
+ lazyMount: true,
37
+ unmountOnExit: true,
38
+ ...props,
39
+ });
40
+ const classNames = contentDialog(variantProps);
41
+
42
+ return (
43
+ <ClassNamesProvider value={classNames}>
44
+ <closeButtonTracker.Provider>
45
+ <DialogPrimitive.Root {...otherProps} />
46
+ </closeButtonTracker.Provider>
47
+ </ClassNamesProvider>
48
+ );
49
+ }
50
+
51
+ ////////////////////////////////////////////////////////////////////////////////////
52
+
53
+ export interface ContentDialogTriggerProps extends DialogPrimitive.TriggerProps {}
54
+
55
+ export const ContentDialogTrigger = DialogPrimitive.Trigger;
56
+
57
+ ////////////////////////////////////////////////////////////////////////////////////
58
+
59
+ export interface ContentDialogPositionerProps extends DialogPrimitive.PositionerProps {}
60
+
61
+ export const ContentDialogPositioner = withContext<HTMLDivElement, ContentDialogPositionerProps>(
62
+ DialogPrimitive.Positioner,
63
+ "positioner",
64
+ );
65
+
66
+ ////////////////////////////////////////////////////////////////////////////////////
67
+
68
+ export interface ContentDialogBackdropProps extends DialogPrimitive.BackdropProps {}
69
+
70
+ export const ContentDialogBackdrop = withContext<HTMLDivElement, ContentDialogBackdropProps>(
71
+ DialogPrimitive.Backdrop,
72
+ "backdrop",
73
+ );
74
+
75
+ ////////////////////////////////////////////////////////////////////////////////////
76
+
77
+ export interface ContentDialogContentProps
78
+ extends DialogPrimitive.ContentProps,
79
+ Pick<StyleProps, "width" | "maxWidth"> {}
80
+
81
+ export const ContentDialogContent = withContext<HTMLDivElement, ContentDialogContentProps>(
82
+ withStyleProps(DialogPrimitive.Content),
83
+ "content",
84
+ );
85
+
86
+ ////////////////////////////////////////////////////////////////////////////////////
87
+
88
+ export interface ContentDialogHeaderProps
89
+ extends PrimitiveProps,
90
+ React.HTMLAttributes<HTMLDivElement> {}
91
+
92
+ export const ContentDialogHeader = React.forwardRef<HTMLDivElement, ContentDialogHeaderProps>(
93
+ ({ className, ...props }, ref) => {
94
+ const classNames = useClassNames();
95
+ const { isRendered } = closeButtonTracker.useRenderTracking();
96
+
97
+ return (
98
+ <Primitive.div
99
+ ref={ref}
100
+ data-show-close-button={dataAttr(isRendered)}
101
+ className={clsx(classNames.header, className)}
102
+ {...props}
103
+ />
104
+ );
105
+ },
106
+ );
107
+
108
+ ContentDialogHeader.displayName = "ContentDialogHeader";
109
+
110
+ ////////////////////////////////////////////////////////////////////////////////////
111
+
112
+ export interface ContentDialogTitleProps extends DialogPrimitive.TitleProps {}
113
+
114
+ export const ContentDialogTitle = withContext<HTMLHeadingElement, ContentDialogTitleProps>(
115
+ DialogPrimitive.Title,
116
+ "title",
117
+ );
118
+
119
+ ////////////////////////////////////////////////////////////////////////////////////
120
+
121
+ export interface ContentDialogDescriptionProps extends DialogPrimitive.DescriptionProps {}
122
+
123
+ export const ContentDialogDescription = withContext<
124
+ HTMLParagraphElement,
125
+ ContentDialogDescriptionProps
126
+ >(DialogPrimitive.Description, "description");
127
+
128
+ ////////////////////////////////////////////////////////////////////////////////////
129
+
130
+ export interface ContentDialogBodyProps
131
+ extends PrimitiveProps,
132
+ Pick<StyleProps, "paddingX" | "minHeight" | "maxHeight" | "justifyContent" | "alignItems">,
133
+ React.HTMLAttributes<HTMLDivElement> {}
134
+
135
+ export const ContentDialogBody = React.forwardRef<HTMLDivElement, ContentDialogBodyProps>(
136
+ (props, forwardedRef) => {
137
+ const classNames = useClassNames();
138
+ const { style, restProps } = useStyleProps(props);
139
+ const { className, ...otherProps } = restProps;
140
+
141
+ const ref = React.useRef<HTMLDivElement>(null);
142
+ const [scrolled, setScrolled] = React.useState(false);
143
+ const [overflowing, setOverflowing] = React.useState(false);
144
+
145
+ React.useEffect(() => {
146
+ const element = ref.current;
147
+ if (!element) return;
148
+
149
+ const check = () => {
150
+ setScrolled(element.scrollTop > 0);
151
+ // Subtract the current bottom padding so overflow detection stays independent
152
+ // of the padding we conditionally apply — otherwise that padding would count as
153
+ // overflow and the state would never settle (padding -> overflow -> padding...).
154
+ const paddingBottom = Number.parseFloat(getComputedStyle(element).paddingBottom) || 0;
155
+ setOverflowing(element.scrollHeight - paddingBottom > element.clientHeight);
156
+ };
157
+ check();
158
+
159
+ element.addEventListener("scroll", check);
160
+
161
+ const observer = new ResizeObserver(check);
162
+ observer.observe(element);
163
+
164
+ return () => {
165
+ element.removeEventListener("scroll", check);
166
+ observer.disconnect();
167
+ };
168
+ }, []);
169
+
170
+ return (
171
+ <Primitive.div
172
+ ref={composeRefs(ref, forwardedRef)}
173
+ data-scrolled={dataAttr(scrolled)}
174
+ data-overflow={dataAttr(overflowing)}
175
+ className={clsx(classNames.body, className)}
176
+ style={style}
177
+ {...otherProps}
178
+ />
179
+ );
180
+ },
181
+ );
182
+
183
+ ContentDialogBody.displayName = "ContentDialogBody";
184
+
185
+ ////////////////////////////////////////////////////////////////////////////////////
186
+
187
+ export interface ContentDialogFooterProps
188
+ extends PrimitiveProps,
189
+ React.HTMLAttributes<HTMLDivElement> {}
190
+
191
+ export const ContentDialogFooter = withContext<HTMLDivElement, ContentDialogFooterProps>(
192
+ Primitive.div,
193
+ "footer",
194
+ );
195
+
196
+ ////////////////////////////////////////////////////////////////////////////////////
197
+
198
+ export interface ContentDialogActionProps extends DialogPrimitive.CloseButtonProps {}
199
+
200
+ export const ContentDialogAction = DialogPrimitive.CloseButton;
201
+
202
+ ////////////////////////////////////////////////////////////////////////////////////
203
+
204
+ export interface ContentDialogCloseButtonProps extends DialogPrimitive.CloseButtonProps {}
205
+
206
+ export const ContentDialogCloseButton = React.forwardRef<
207
+ HTMLButtonElement,
208
+ ContentDialogCloseButtonProps
209
+ >(({ className, ...props }, ref) => {
210
+ const classNames = useClassNames();
211
+ const { trackRef } = closeButtonTracker.useRenderTracking();
212
+
213
+ return (
214
+ <DialogPrimitive.CloseButton
215
+ ref={composeRefs(ref, trackRef)}
216
+ className={clsx(classNames.closeButton, className)}
217
+ {...props}
218
+ />
219
+ );
220
+ });
221
+
222
+ ContentDialogCloseButton.displayName = "ContentDialogCloseButton";
@@ -0,0 +1,33 @@
1
+ // NOTE: dialog naming is mid-rename; rootage/vars already use the new names, recipes and react components still use the old ones.
2
+ // Semantically (= snippet naming), this component is the Dialog:
3
+ // snippet AlertDialog → react Dialog → recipe "dialog" → vars alertDialog
4
+ // snippet Dialog → react ContentDialog → recipe "content-dialog" → vars dialog
5
+
6
+ export {
7
+ ContentDialogBackdrop,
8
+ ContentDialogPositioner,
9
+ ContentDialogContent,
10
+ ContentDialogBody,
11
+ ContentDialogDescription,
12
+ ContentDialogFooter,
13
+ ContentDialogHeader,
14
+ ContentDialogRoot,
15
+ ContentDialogTitle,
16
+ ContentDialogTrigger,
17
+ ContentDialogAction,
18
+ ContentDialogCloseButton,
19
+ type ContentDialogBackdropProps,
20
+ type ContentDialogPositionerProps,
21
+ type ContentDialogContentProps,
22
+ type ContentDialogBodyProps,
23
+ type ContentDialogDescriptionProps,
24
+ type ContentDialogFooterProps,
25
+ type ContentDialogHeaderProps,
26
+ type ContentDialogRootProps,
27
+ type ContentDialogTitleProps,
28
+ type ContentDialogTriggerProps,
29
+ type ContentDialogActionProps,
30
+ type ContentDialogCloseButtonProps,
31
+ } from "./ContentDialog";
32
+
33
+ export * as ContentDialog from "./ContentDialog.namespace";
@@ -1,3 +1,8 @@
1
+ // NOTE: dialog naming is mid-rename; rootage/vars already use the new names, recipes and react components still use the old ones.
2
+ // Semantically (= snippet naming), this component is the AlertDialog:
3
+ // snippet AlertDialog → react Dialog → recipe "dialog" → vars alertDialog
4
+ // snippet Dialog → react ContentDialog → recipe "content-dialog" → vars dialog
5
+
1
6
  export {
2
7
  DialogBackdrop,
3
8
  DialogPositioner,
@@ -0,0 +1,12 @@
1
+ export {
2
+ QuantityPickerRoot as Root,
3
+ QuantityPickerDecrementButton as DecrementButton,
4
+ QuantityPickerValueDisplay as ValueDisplay,
5
+ QuantityPickerIncrementButton as IncrementButton,
6
+ QuantityPickerHiddenInput as HiddenInput,
7
+ type QuantityPickerRootProps as RootProps,
8
+ type QuantityPickerDecrementButtonProps as DecrementButtonProps,
9
+ type QuantityPickerValueDisplayProps as ValueDisplayProps,
10
+ type QuantityPickerIncrementButtonProps as IncrementButtonProps,
11
+ type QuantityPickerHiddenInputProps as HiddenInputProps,
12
+ } from "./QuantityPicker";
@@ -0,0 +1,108 @@
1
+ import { render } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it } from "bun:test";
3
+ import { QuantityPicker } from "./index";
4
+
5
+ afterEach(() => {
6
+ document.body.replaceChildren();
7
+ });
8
+
9
+ describe("QuantityPicker", () => {
10
+ it("값 표시 슬롯 양옆에 divider DOM을 배치하고 action icon slot을 연결한다", () => {
11
+ const { getByLabelText, getByText, container } = render(
12
+ <QuantityPicker.Root defaultValue={1} min={0} max={5} aria-label="수량">
13
+ <QuantityPicker.DecrementButton aria-label="줄이기" icon={<svg />} />
14
+ <QuantityPicker.ValueDisplay />
15
+ <QuantityPicker.IncrementButton aria-label="늘리기" icon={<svg />} />
16
+ <QuantityPicker.HiddenInput name="quantity" />
17
+ </QuantityPicker.Root>,
18
+ );
19
+
20
+ const valueDisplay = getByText("1").closest(".seed-quantity-picker__valueDisplay");
21
+ if (!valueDisplay) throw new Error("값 표시를 찾을 수 없습니다.");
22
+ const decrementButton = getByLabelText("줄이기");
23
+ const incrementButton = getByLabelText("늘리기");
24
+
25
+ expect(valueDisplay).toHaveClass("seed-quantity-picker__valueDisplay");
26
+ expect(valueDisplay.previousElementSibling).toHaveClass("seed-quantity-picker__divider");
27
+ expect(valueDisplay.nextElementSibling).toHaveClass("seed-quantity-picker__divider");
28
+ expect(valueDisplay.previousElementSibling?.previousElementSibling).toBe(decrementButton);
29
+ expect(valueDisplay.nextElementSibling?.nextElementSibling).toBe(incrementButton);
30
+ expect(getByLabelText("줄이기").firstElementChild).toHaveClass(
31
+ "seed-quantity-picker__decrementIcon",
32
+ );
33
+ expect(getByLabelText("줄이기").firstElementChild).toHaveAttribute("aria-hidden", "true");
34
+ expect(getByLabelText("늘리기").firstElementChild).toHaveClass(
35
+ "seed-quantity-picker__incrementIcon",
36
+ );
37
+ expect(container.querySelectorAll(".seed-quantity-picker__divider")).toHaveLength(2);
38
+ });
39
+
40
+ it("fallback children은 버튼의 접근 가능한 이름으로 유지한다", () => {
41
+ const { getByRole } = render(
42
+ <QuantityPicker.Root min={0} max={5} aria-label="수량">
43
+ <QuantityPicker.DecrementButton>수량 줄이기</QuantityPicker.DecrementButton>
44
+ <QuantityPicker.ValueDisplay />
45
+ <QuantityPicker.IncrementButton>수량 늘리기</QuantityPicker.IncrementButton>
46
+ </QuantityPicker.Root>,
47
+ );
48
+
49
+ expect(getByRole("button", { name: "수량 줄이기" })).toBeInTheDocument();
50
+ expect(getByRole("button", { name: "수량 늘리기" })).toBeInTheDocument();
51
+ });
52
+
53
+ it("직접 인접한 action과 값 표시 사이에만 divider를 삽입한다", () => {
54
+ const { container } = render(
55
+ <QuantityPicker.Root min={0} max={5} aria-label="수량">
56
+ <QuantityPicker.DecrementButton aria-label="줄이기" icon={<svg />} />
57
+ <QuantityPicker.ValueDisplay />
58
+ <QuantityPicker.HiddenInput name="quantity" />
59
+ </QuantityPicker.Root>,
60
+ );
61
+
62
+ expect(container.querySelectorAll(".seed-quantity-picker__divider")).toHaveLength(1);
63
+ });
64
+
65
+ it("최대값의 자릿수만큼 0으로 채운 숨김 요소로 값 표시 너비를 확보한다", () => {
66
+ const { container } = render(
67
+ <QuantityPicker.Root min={0} max={123} aria-label="수량">
68
+ <QuantityPicker.DecrementButton aria-label="줄이기" icon={<svg />} />
69
+ <QuantityPicker.ValueDisplay />
70
+ <QuantityPicker.IncrementButton aria-label="늘리기" icon={<svg />} />
71
+ </QuantityPicker.Root>,
72
+ );
73
+
74
+ const placeholder = container.querySelector(".seed-quantity-picker__valueDisplayPlaceholder");
75
+
76
+ expect(placeholder).toHaveAttribute("aria-hidden", "true");
77
+ expect(placeholder).toHaveTextContent("000");
78
+ });
79
+
80
+ it("숨김 요소에도 getValueText를 적용해 포매팅된 값의 너비를 확보한다", () => {
81
+ const { container } = render(
82
+ <QuantityPicker.Root
83
+ min={0}
84
+ max={123}
85
+ getValueText={(valueText, value) => `${valueText}개(${value.toString()})`}
86
+ aria-label="수량"
87
+ >
88
+ <QuantityPicker.ValueDisplay />
89
+ </QuantityPicker.Root>,
90
+ );
91
+
92
+ const placeholder = container.querySelector(".seed-quantity-picker__valueDisplayPlaceholder");
93
+
94
+ expect(placeholder).toHaveTextContent("000개(000)");
95
+ });
96
+
97
+ it("범위 끝에서는 해당 action icon에도 disabled state를 전달한다", () => {
98
+ const { getByLabelText } = render(
99
+ <QuantityPicker.Root min={0} max={1} value={1} aria-label="수량">
100
+ <QuantityPicker.DecrementButton aria-label="줄이기" icon={<svg />} />
101
+ <QuantityPicker.ValueDisplay />
102
+ <QuantityPicker.IncrementButton aria-label="늘리기" icon={<svg />} />
103
+ </QuantityPicker.Root>,
104
+ );
105
+
106
+ expect(getByLabelText("늘리기").firstElementChild).toHaveAttribute("data-disabled");
107
+ });
108
+ });
@@ -0,0 +1,214 @@
1
+ "use client";
2
+
3
+ import { mergeProps } from "@seed-design/dom-utils";
4
+ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
5
+ import {
6
+ QuantityPicker as QuantityPickerPrimitive,
7
+ useQuantityPickerContext,
8
+ } from "@seed-design/react-quantity-picker";
9
+ import {
10
+ quantityPicker,
11
+ type QuantityPickerVariantProps,
12
+ } from "@seed-design/css/recipes/quantity-picker";
13
+ import clsx from "clsx";
14
+ import * as React from "react";
15
+ import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
16
+ import { createWithStateProps } from "../../utils/createWithStateProps";
17
+
18
+ const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(quantityPicker);
19
+ const withStateProps = createWithStateProps([useQuantityPickerContext]);
20
+
21
+ function isQuantityPickerButton(child: React.ReactNode) {
22
+ return (
23
+ React.isValidElement(child) &&
24
+ (child.type === QuantityPickerDecrementButton || child.type === QuantityPickerIncrementButton)
25
+ );
26
+ }
27
+
28
+ function isQuantityPickerValueDisplay(child: React.ReactNode) {
29
+ return React.isValidElement(child) && child.type === QuantityPickerValueDisplay;
30
+ }
31
+
32
+ function withDividers(children: React.ReactNode, dividerClassName: string) {
33
+ const childArray = React.Children.toArray(children);
34
+
35
+ return childArray.flatMap((child, index) => {
36
+ const nextChild = childArray[index + 1];
37
+ const shouldInsertDivider =
38
+ (isQuantityPickerButton(child) && isQuantityPickerValueDisplay(nextChild)) ||
39
+ (isQuantityPickerValueDisplay(child) && isQuantityPickerButton(nextChild));
40
+
41
+ if (!shouldInsertDivider) return child;
42
+
43
+ return [
44
+ child,
45
+ <Primitive.div
46
+ key={`divider-${index.toString()}`}
47
+ aria-hidden="true"
48
+ className={dividerClassName}
49
+ />,
50
+ ];
51
+ });
52
+ }
53
+
54
+ function getValueDisplayPlaceholder(max: number) {
55
+ return String(max).replace(/\d/g, "0");
56
+ }
57
+
58
+ function renderIcon(
59
+ icon: React.ReactNode,
60
+ className: string,
61
+ stateProps: { "data-disabled"?: string },
62
+ ariaHidden: boolean,
63
+ ) {
64
+ if (!React.isValidElement<{ className?: string }>(icon)) return icon;
65
+
66
+ return React.cloneElement(
67
+ icon as React.ReactElement<{
68
+ className?: string;
69
+ "aria-hidden"?: boolean;
70
+ "data-disabled"?: string;
71
+ }>,
72
+ {
73
+ ...(ariaHidden ? { "aria-hidden": true } : {}),
74
+ className: clsx(icon.props.className, className),
75
+ "data-disabled": stateProps["data-disabled"],
76
+ },
77
+ );
78
+ }
79
+
80
+ export type QuantityPickerRootProps = QuantityPickerVariantProps &
81
+ QuantityPickerPrimitive.RootProps;
82
+
83
+ export const QuantityPickerRoot = React.forwardRef<HTMLDivElement, QuantityPickerRootProps>(
84
+ (props, ref) => {
85
+ const [variantProps, otherProps] = quantityPicker.splitVariantProps(props);
86
+ const {
87
+ children,
88
+ className,
89
+ removable: _removable,
90
+ removeAriaLabel: _removeAriaLabel,
91
+ ...rootProps
92
+ } = otherProps;
93
+ const classNames = quantityPicker(variantProps);
94
+ const quantityPickerRootProps: QuantityPickerPrimitive.RootProps = props.removable
95
+ ? {
96
+ ...rootProps,
97
+ removable: true,
98
+ removeAriaLabel: props.removeAriaLabel,
99
+ }
100
+ : {
101
+ ...rootProps,
102
+ removable: false,
103
+ };
104
+
105
+ return (
106
+ <ClassNamesProvider value={classNames}>
107
+ <QuantityPickerPrimitive.Root
108
+ ref={ref}
109
+ className={clsx(classNames.root, className)}
110
+ {...quantityPickerRootProps}
111
+ >
112
+ {withDividers(children, classNames.divider)}
113
+ </QuantityPickerPrimitive.Root>
114
+ </ClassNamesProvider>
115
+ );
116
+ },
117
+ );
118
+ QuantityPickerRoot.displayName = "QuantityPickerRoot";
119
+
120
+ interface QuantityPickerButtonProps
121
+ extends PrimitiveProps,
122
+ Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "disabled" | "type"> {
123
+ icon?: React.ReactNode;
124
+ loadingIndicator?: React.ReactNode;
125
+ }
126
+
127
+ export interface QuantityPickerDecrementButtonProps
128
+ extends Omit<QuantityPickerPrimitive.DecrementButtonProps, "children">,
129
+ QuantityPickerButtonProps {
130
+ removeIcon?: React.ReactNode;
131
+ }
132
+
133
+ export const QuantityPickerDecrementButton = React.forwardRef<
134
+ HTMLButtonElement,
135
+ QuantityPickerDecrementButtonProps
136
+ >(({ children, className, icon, loadingIndicator, removeIcon, ...props }, ref) => {
137
+ const classNames = useClassNames();
138
+ const { decrementIconProps, decrementLoading, isRemoveButton } = useQuantityPickerContext();
139
+ const content = decrementLoading
140
+ ? loadingIndicator
141
+ : isRemoveButton
142
+ ? (removeIcon ?? icon)
143
+ : icon;
144
+ const contentIsIcon = content !== null && content !== undefined;
145
+
146
+ return (
147
+ <QuantityPickerPrimitive.DecrementButton
148
+ ref={ref}
149
+ className={clsx(classNames.decrementButton, className)}
150
+ {...props}
151
+ >
152
+ {renderIcon(content ?? children, classNames.decrementIcon, decrementIconProps, contentIsIcon)}
153
+ </QuantityPickerPrimitive.DecrementButton>
154
+ );
155
+ });
156
+ QuantityPickerDecrementButton.displayName = "QuantityPickerDecrementButton";
157
+
158
+ export interface QuantityPickerValueDisplayProps
159
+ extends QuantityPickerPrimitive.ValueDisplayProps {}
160
+
161
+ export const QuantityPickerValueDisplay = withStateProps(
162
+ React.forwardRef<HTMLSpanElement, QuantityPickerValueDisplayProps>(
163
+ ({ className, ...props }, ref) => {
164
+ const classNames = useClassNames();
165
+ const { getValueText, max, valueDisplayProps } = useQuantityPickerContext();
166
+ const placeholder = getValueDisplayPlaceholder(max);
167
+
168
+ return (
169
+ <Primitive.span
170
+ ref={ref}
171
+ {...mergeProps(valueDisplayProps, props)}
172
+ className={clsx(classNames.valueDisplay, className)}
173
+ >
174
+ <Primitive.span aria-hidden="true" className={classNames.valueDisplayPlaceholder}>
175
+ {getValueText(placeholder, placeholder)}
176
+ </Primitive.span>
177
+ <Primitive.span className={classNames.valueDisplayText}>
178
+ {valueDisplayProps.children}
179
+ </Primitive.span>
180
+ </Primitive.span>
181
+ );
182
+ },
183
+ ),
184
+ );
185
+ QuantityPickerValueDisplay.displayName = "QuantityPickerValueDisplay";
186
+
187
+ export interface QuantityPickerIncrementButtonProps
188
+ extends Omit<QuantityPickerPrimitive.IncrementButtonProps, "children">,
189
+ QuantityPickerButtonProps {}
190
+
191
+ export const QuantityPickerIncrementButton = React.forwardRef<
192
+ HTMLButtonElement,
193
+ QuantityPickerIncrementButtonProps
194
+ >(({ children, className, icon, loadingIndicator, ...props }, ref) => {
195
+ const classNames = useClassNames();
196
+ const { incrementIconProps, incrementLoading } = useQuantityPickerContext();
197
+ const content = incrementLoading ? loadingIndicator : icon;
198
+ const contentIsIcon = content !== null && content !== undefined;
199
+
200
+ return (
201
+ <QuantityPickerPrimitive.IncrementButton
202
+ ref={ref}
203
+ className={clsx(classNames.incrementButton, className)}
204
+ {...props}
205
+ >
206
+ {renderIcon(content ?? children, classNames.incrementIcon, incrementIconProps, contentIsIcon)}
207
+ </QuantityPickerPrimitive.IncrementButton>
208
+ );
209
+ });
210
+ QuantityPickerIncrementButton.displayName = "QuantityPickerIncrementButton";
211
+
212
+ export interface QuantityPickerHiddenInputProps extends QuantityPickerPrimitive.HiddenInputProps {}
213
+
214
+ export const QuantityPickerHiddenInput = QuantityPickerPrimitive.HiddenInput;
@@ -0,0 +1,14 @@
1
+ export {
2
+ QuantityPickerRoot,
3
+ QuantityPickerDecrementButton,
4
+ QuantityPickerValueDisplay,
5
+ QuantityPickerIncrementButton,
6
+ QuantityPickerHiddenInput,
7
+ type QuantityPickerRootProps,
8
+ type QuantityPickerDecrementButtonProps,
9
+ type QuantityPickerValueDisplayProps,
10
+ type QuantityPickerIncrementButtonProps,
11
+ type QuantityPickerHiddenInputProps,
12
+ } from "./QuantityPicker";
13
+
14
+ export * as QuantityPicker from "./QuantityPicker.namespace";
@@ -0,0 +1,28 @@
1
+ export {
2
+ ResponsiveDialogAction as Action,
3
+ ResponsiveDialogBackdrop as Backdrop,
4
+ ResponsiveDialogBody as Body,
5
+ ResponsiveDialogContent as Content,
6
+ ResponsiveDialogDescription as Description,
7
+ ResponsiveDialogFooter as Footer,
8
+ ResponsiveDialogHandle as Handle,
9
+ ResponsiveDialogHeader as Header,
10
+ ResponsiveDialogPositioner as Positioner,
11
+ ResponsiveDialogRoot as Root,
12
+ ResponsiveDialogTitle as Title,
13
+ ResponsiveDialogTrigger as Trigger,
14
+ ResponsiveDialogCloseButton as CloseButton,
15
+ type ResponsiveDialogActionProps as ActionProps,
16
+ type ResponsiveDialogBackdropProps as BackdropProps,
17
+ type ResponsiveDialogBodyProps as BodyProps,
18
+ type ResponsiveDialogContentProps as ContentProps,
19
+ type ResponsiveDialogDescriptionProps as DescriptionProps,
20
+ type ResponsiveDialogFooterProps as FooterProps,
21
+ type ResponsiveDialogHandleProps as HandleProps,
22
+ type ResponsiveDialogHeaderProps as HeaderProps,
23
+ type ResponsiveDialogPositionerProps as PositionerProps,
24
+ type ResponsiveDialogRootProps as RootProps,
25
+ type ResponsiveDialogTitleProps as TitleProps,
26
+ type ResponsiveDialogTriggerProps as TriggerProps,
27
+ type ResponsiveDialogCloseButtonProps as CloseButtonProps,
28
+ } from "./ResponsiveDialog";