@seed-design/react 2.3.0 → 2.4.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 (172) hide show
  1. package/lib/components/ActionButton/ActionButton.cjs +5 -2
  2. package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
  3. package/lib/components/ActionButton/ActionButton.js +5 -2
  4. package/lib/components/AttachmentDisplay/AttachmentDisplayItem.cjs +10 -6
  5. package/lib/components/AttachmentDisplay/AttachmentDisplayItem.d.ts.map +1 -1
  6. package/lib/components/AttachmentDisplay/AttachmentDisplayItem.js +10 -6
  7. package/lib/components/AttachmentDisplay/AttachmentDisplayTrigger.cjs +2 -1
  8. package/lib/components/AttachmentDisplay/AttachmentDisplayTrigger.d.ts.map +1 -1
  9. package/lib/components/AttachmentDisplay/AttachmentDisplayTrigger.js +2 -1
  10. package/lib/components/AttachmentInput/AttachmentInputItem.cjs +10 -6
  11. package/lib/components/AttachmentInput/AttachmentInputItem.d.ts.map +1 -1
  12. package/lib/components/AttachmentInput/AttachmentInputItem.js +10 -6
  13. package/lib/components/AttachmentInput/AttachmentInputTrigger.cjs +2 -1
  14. package/lib/components/AttachmentInput/AttachmentInputTrigger.d.ts.map +1 -1
  15. package/lib/components/AttachmentInput/AttachmentInputTrigger.js +2 -1
  16. package/lib/components/BottomSheet/BottomSheet.cjs +5 -3
  17. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  18. package/lib/components/BottomSheet/BottomSheet.js +5 -3
  19. package/lib/components/Callout/Callout.cjs +3 -2
  20. package/lib/components/Callout/Callout.d.ts.map +1 -1
  21. package/lib/components/Callout/Callout.js +3 -2
  22. package/lib/components/Checkbox/Checkbox.cjs +2 -1
  23. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
  24. package/lib/components/Checkbox/Checkbox.js +2 -1
  25. package/lib/components/Chip/Chip.cjs +2 -1
  26. package/lib/components/Chip/Chip.d.ts.map +1 -1
  27. package/lib/components/Chip/Chip.js +2 -1
  28. package/lib/components/ChipTabs/ChipTabs.cjs +2 -1
  29. package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -1
  30. package/lib/components/ChipTabs/ChipTabs.js +2 -1
  31. package/lib/components/ContentDialog/ContentDialog.cjs +5 -3
  32. package/lib/components/ContentDialog/ContentDialog.d.ts.map +1 -1
  33. package/lib/components/ContentDialog/ContentDialog.js +5 -3
  34. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.cjs +5 -2
  35. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -1
  36. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.js +5 -2
  37. package/lib/components/DatePicker/DatePicker.cjs +2 -2
  38. package/lib/components/DatePicker/DatePicker.d.ts +10 -4
  39. package/lib/components/DatePicker/DatePicker.d.ts.map +1 -1
  40. package/lib/components/DatePicker/DatePicker.js +2 -2
  41. package/lib/components/DatePicker/index.d.ts +1 -1
  42. package/lib/components/DatePicker/index.d.ts.map +1 -1
  43. package/lib/components/FieldButton/FieldButton.cjs +2 -1
  44. package/lib/components/FieldButton/FieldButton.d.ts.map +1 -1
  45. package/lib/components/FieldButton/FieldButton.js +2 -1
  46. package/lib/components/FloatingActionButton/FloatingActionButton.cjs +2 -1
  47. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -1
  48. package/lib/components/FloatingActionButton/FloatingActionButton.js +2 -1
  49. package/lib/components/HelpBubble/HelpBubble.cjs +2 -1
  50. package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -1
  51. package/lib/components/HelpBubble/HelpBubble.js +2 -1
  52. package/lib/components/ImageFrame/ImageFrame.cjs +5 -2
  53. package/lib/components/ImageFrame/ImageFrame.d.ts.map +1 -1
  54. package/lib/components/ImageFrame/ImageFrame.js +5 -2
  55. package/lib/components/LoadingIndicator/usePendingButton.d.ts +57 -57
  56. package/lib/components/MenuSheet/MenuSheet.cjs +2 -1
  57. package/lib/components/MenuSheet/MenuSheet.d.ts.map +1 -1
  58. package/lib/components/MenuSheet/MenuSheet.js +2 -1
  59. package/lib/components/PageBanner/PageBanner.cjs +3 -2
  60. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
  61. package/lib/components/PageBanner/PageBanner.js +3 -2
  62. package/lib/components/QuantityPicker/QuantityPicker.cjs +8 -4
  63. package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -1
  64. package/lib/components/QuantityPicker/QuantityPicker.js +8 -4
  65. package/lib/components/RadioGroup/RadioGroup.cjs +2 -1
  66. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  67. package/lib/components/RadioGroup/RadioGroup.js +2 -1
  68. package/lib/components/ReactionButton/ReactionButton.cjs +5 -2
  69. package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
  70. package/lib/components/ReactionButton/ReactionButton.js +5 -2
  71. package/lib/components/ScrollFog/ScrollFog.d.ts +4 -6
  72. package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -1
  73. package/lib/components/ScrollFog/ScrollFog.test.d.ts +2 -0
  74. package/lib/components/ScrollFog/ScrollFog.test.d.ts.map +1 -0
  75. package/lib/components/SideNavigation/SideNavigation.cjs +1 -1
  76. package/lib/components/SideNavigation/SideNavigation.js +1 -1
  77. package/lib/components/SidePanel/SidePanel.cjs +5 -3
  78. package/lib/components/SidePanel/SidePanel.d.ts.map +1 -1
  79. package/lib/components/SidePanel/SidePanel.js +5 -3
  80. package/lib/components/Slider/Slider.cjs +1 -1
  81. package/lib/components/Slider/Slider.js +1 -1
  82. package/lib/components/Snackbar/Snackbar.cjs +2 -1
  83. package/lib/components/Snackbar/Snackbar.d.ts.map +1 -1
  84. package/lib/components/Snackbar/Snackbar.js +2 -1
  85. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.cjs +2 -1
  86. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
  87. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.js +2 -1
  88. package/lib/components/Switch/Switch.cjs +2 -1
  89. package/lib/components/Switch/Switch.d.ts.map +1 -1
  90. package/lib/components/Switch/Switch.js +2 -1
  91. package/lib/components/Tabs/Tabs.cjs +2 -1
  92. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  93. package/lib/components/Tabs/Tabs.js +2 -1
  94. package/lib/components/TextField/TextField.cjs +1 -1
  95. package/lib/components/TextField/TextField.js +1 -1
  96. package/lib/components/ToggleButton/ToggleButton.cjs +5 -2
  97. package/lib/components/ToggleButton/ToggleButton.d.ts.map +1 -1
  98. package/lib/components/ToggleButton/ToggleButton.js +5 -2
  99. package/lib/components/WheelPicker/WheelPicker.cjs +94 -0
  100. package/lib/components/WheelPicker/WheelPicker.d.ts +56 -0
  101. package/lib/components/WheelPicker/WheelPicker.d.ts.map +1 -0
  102. package/lib/components/WheelPicker/WheelPicker.js +89 -0
  103. package/lib/components/WheelPicker/WheelPicker.namespace.cjs +15 -0
  104. package/lib/components/WheelPicker/WheelPicker.namespace.d.ts +2 -0
  105. package/lib/components/WheelPicker/WheelPicker.namespace.d.ts.map +1 -0
  106. package/lib/components/WheelPicker/WheelPicker.namespace.js +10 -0
  107. package/lib/components/WheelPicker/WheelPicker.test.d.ts +2 -0
  108. package/lib/components/WheelPicker/WheelPicker.test.d.ts.map +1 -0
  109. package/lib/components/WheelPicker/index.cjs +12 -0
  110. package/lib/components/WheelPicker/index.d.ts +3 -0
  111. package/lib/components/WheelPicker/index.d.ts.map +1 -0
  112. package/lib/components/WheelPicker/index.js +3 -0
  113. package/lib/components/index.cjs +11 -0
  114. package/lib/components/index.d.ts +1 -0
  115. package/lib/components/index.d.ts.map +1 -1
  116. package/lib/components/index.js +3 -1
  117. package/lib/components/private/WheelPicker.cjs +2 -3
  118. package/lib/components/private/WheelPicker.d.ts +3 -3
  119. package/lib/components/private/WheelPicker.d.ts.map +1 -1
  120. package/lib/components/private/WheelPicker.js +3 -3
  121. package/lib/components/private/useDismissible.d.ts +123 -123
  122. package/lib/index.cjs +24 -0
  123. package/lib/index.d.ts +2 -0
  124. package/lib/index.d.ts.map +1 -1
  125. package/lib/index.js +4 -1
  126. package/lib/utils/withScaleFeedback.cjs +25 -0
  127. package/lib/utils/withScaleFeedback.d.ts +8 -0
  128. package/lib/utils/withScaleFeedback.d.ts.map +1 -0
  129. package/lib/utils/withScaleFeedback.js +22 -0
  130. package/package.json +6 -5
  131. package/src/components/ActionButton/ActionButton.tsx +5 -2
  132. package/src/components/AttachmentDisplay/AttachmentDisplayItem.tsx +15 -9
  133. package/src/components/AttachmentDisplay/AttachmentDisplayTrigger.tsx +7 -4
  134. package/src/components/AttachmentInput/AttachmentInputItem.tsx +15 -9
  135. package/src/components/AttachmentInput/AttachmentInputTrigger.tsx +3 -3
  136. package/src/components/BottomSheet/BottomSheet.tsx +5 -3
  137. package/src/components/Callout/Callout.tsx +9 -4
  138. package/src/components/Checkbox/Checkbox.tsx +3 -3
  139. package/src/components/Chip/Chip.tsx +2 -1
  140. package/src/components/ChipTabs/ChipTabs.tsx +3 -3
  141. package/src/components/ContentDialog/ContentDialog.tsx +5 -3
  142. package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +5 -2
  143. package/src/components/DatePicker/DatePicker.test.tsx +20 -0
  144. package/src/components/DatePicker/DatePicker.tsx +14 -2
  145. package/src/components/DatePicker/index.ts +2 -0
  146. package/src/components/FieldButton/FieldButton.tsx +6 -3
  147. package/src/components/FloatingActionButton/FloatingActionButton.tsx +4 -4
  148. package/src/components/HelpBubble/HelpBubble.tsx +7 -4
  149. package/src/components/HelpBubbleTooltip/HelpBubbleTooltip.tsx +1 -1
  150. package/src/components/ImageFrame/ImageFrame.tsx +8 -1
  151. package/src/components/MenuSheet/MenuSheet.tsx +6 -3
  152. package/src/components/PageBanner/PageBanner.tsx +5 -6
  153. package/src/components/QuantityPicker/QuantityPicker.tsx +8 -4
  154. package/src/components/RadioGroup/RadioGroup.tsx +7 -4
  155. package/src/components/ReactionButton/ReactionButton.tsx +5 -2
  156. package/src/components/ScrollFog/ScrollFog.test.tsx +13 -0
  157. package/src/components/ScrollFog/ScrollFog.tsx +5 -7
  158. package/src/components/SidePanel/SidePanel.tsx +5 -3
  159. package/src/components/Snackbar/Snackbar.tsx +6 -3
  160. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +7 -4
  161. package/src/components/Switch/Switch.tsx +3 -3
  162. package/src/components/Tabs/Tabs.tsx +3 -3
  163. package/src/components/ToggleButton/ToggleButton.tsx +5 -2
  164. package/src/components/WheelPicker/WheelPicker.namespace.ts +9 -0
  165. package/src/components/WheelPicker/WheelPicker.test.tsx +270 -0
  166. package/src/components/WheelPicker/WheelPicker.tsx +210 -0
  167. package/src/components/WheelPicker/index.ts +11 -0
  168. package/src/components/index.ts +1 -0
  169. package/src/components/private/WheelPicker.test.tsx +1 -0
  170. package/src/components/private/WheelPicker.tsx +6 -6
  171. package/src/index.ts +2 -0
  172. package/src/utils/withScaleFeedback.tsx +34 -0
@@ -0,0 +1,270 @@
1
+ import { fireEvent, render } from "@testing-library/react";
2
+ import { describe, expect, it, mock, spyOn } from "bun:test";
3
+ import type * as React from "react";
4
+ import { BottomSheet } from "../BottomSheet";
5
+ import { WheelPicker } from "./index";
6
+
7
+ const options = [
8
+ { value: "a", label: "A" },
9
+ { value: "b", label: "B" },
10
+ ];
11
+
12
+ function fireTouchPointer(type: string, element: HTMLElement, y: number) {
13
+ const event = new PointerEvent(type, {
14
+ bubbles: true,
15
+ cancelable: true,
16
+ pointerId: 1,
17
+ pointerType: "touch",
18
+ clientX: 50,
19
+ clientY: y,
20
+ });
21
+ Object.defineProperty(event, "pageX", { value: 50 });
22
+ Object.defineProperty(event, "pageY", { value: y });
23
+ fireEvent(element, event);
24
+ }
25
+
26
+ describe("WheelPicker", () => {
27
+ it("공개 기본 geometry와 중립 스타일을 적용한다", () => {
28
+ const { container, getByRole } = render(
29
+ <WheelPicker.Root aria-label="휠 피커">
30
+ <WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
31
+ </WheelPicker.Root>,
32
+ );
33
+
34
+ expect(getByRole("group")).toHaveStyle({
35
+ "--seed-wheel-picker-public-item-size": "44px",
36
+ "--seed-wheel-picker-public-visible-item-count": "5",
37
+ "--seed-wheel-picker-public-viewport-size": "220px",
38
+ "--seed-wheel-picker-public-center-offset": "88px",
39
+ "--seed-wheel-picker-public-scroll-fog-max-height": "132px",
40
+ });
41
+ expect(getByRole("group")).toHaveClass(
42
+ "seed-wheel-picker-public__root",
43
+ "seed-wheel-picker-public__root--size_medium",
44
+ );
45
+ expect(getByRole("group")).not.toHaveClass("seed-wheel-picker__root");
46
+ expect(container.querySelector("[data-wheel-picker-scroll-fog]")).toHaveClass(
47
+ "seed-wheel-picker-public__scrollFog",
48
+ );
49
+ expect(container.querySelector("[data-wheel-picker-columns]")).toHaveClass(
50
+ "seed-wheel-picker-public__columns",
51
+ );
52
+ expect(getByRole("spinbutton")).toHaveClass("seed-wheel-picker-public__column");
53
+ expect(container.querySelector('[data-wheel-picker-value="a"]')).toHaveClass(
54
+ "seed-wheel-picker-public__item",
55
+ );
56
+ expect(container.querySelector("[data-wheel-picker-item-label]")).toHaveClass(
57
+ "seed-wheel-picker-public__itemLabel",
58
+ );
59
+ expect(container.querySelector("[data-wheel-picker-indicator]")).toHaveClass(
60
+ "seed-wheel-picker-public__selectionIndicator",
61
+ );
62
+ const scrollFog = container.querySelector<HTMLElement>("[data-wheel-picker-scroll-fog]");
63
+ expect(scrollFog?.style.getPropertyValue("--scroll-fog-size-top")).toBe(
64
+ "var(--seed-wheel-picker-public-scroll-fog-size)",
65
+ );
66
+ expect(scrollFog?.style.getPropertyValue("--scroll-fog-size-bottom")).toBe(
67
+ "var(--seed-wheel-picker-public-scroll-fog-size)",
68
+ );
69
+ });
70
+
71
+ it("Small 크기의 geometry와 타이포그래피 Variant를 적용한다", () => {
72
+ const { container, getByRole } = render(
73
+ <WheelPicker.Root aria-label="작은 휠 피커" size="small">
74
+ <WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
75
+ </WheelPicker.Root>,
76
+ );
77
+
78
+ expect(getByRole("group")).toHaveStyle({
79
+ "--seed-wheel-picker-public-item-size": "36px",
80
+ "--seed-wheel-picker-public-visible-item-count": "5",
81
+ "--seed-wheel-picker-public-viewport-size": "180px",
82
+ "--seed-wheel-picker-public-center-offset": "72px",
83
+ "--seed-wheel-picker-public-scroll-fog-max-height": "108px",
84
+ });
85
+ expect(getByRole("group")).toHaveClass("seed-wheel-picker-public__root--size_small");
86
+ expect(container.querySelector("[data-wheel-picker-item-label]")).toHaveClass(
87
+ "seed-wheel-picker-public__itemLabel--size_small",
88
+ );
89
+ });
90
+
91
+ it("itemSize를 지정하면 Size의 Item 높이와 Scroll Fog 최대 높이만 덮어쓴다", () => {
92
+ const { container, getByRole } = render(
93
+ <WheelPicker.Root
94
+ aria-label="사용자 지정 휠 피커"
95
+ size="small"
96
+ itemSize={56}
97
+ visibleItemCount={7}
98
+ >
99
+ <WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
100
+ </WheelPicker.Root>,
101
+ );
102
+
103
+ expect(getByRole("group")).toHaveStyle({
104
+ "--seed-wheel-picker-public-item-size": "56px",
105
+ "--seed-wheel-picker-public-visible-item-count": "7",
106
+ "--seed-wheel-picker-public-viewport-size": "392px",
107
+ "--seed-wheel-picker-public-center-offset": "168px",
108
+ "--seed-wheel-picker-public-scroll-fog-max-height": "168px",
109
+ });
110
+ expect(container.querySelector("[data-wheel-picker-item-label]")).toHaveClass(
111
+ "seed-wheel-picker-public__itemLabel--size_small",
112
+ );
113
+ });
114
+
115
+ it("Item을 9개 표시해도 Scroll Fog 최대 높이는 Item 3개 높이로 유지한다", () => {
116
+ const { getByRole } = render(
117
+ <WheelPicker.Root aria-label="긴 휠 피커" visibleItemCount={9}>
118
+ <WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
119
+ </WheelPicker.Root>,
120
+ );
121
+
122
+ expect(getByRole("group")).toHaveStyle({
123
+ "--seed-wheel-picker-public-viewport-size": "396px",
124
+ "--seed-wheel-picker-public-scroll-fog-max-height": "132px",
125
+ });
126
+ });
127
+
128
+ it("Root와 Column의 className을 허용한다", () => {
129
+ const { getByRole } = render(
130
+ <WheelPicker.Root aria-label="휠 피커" className="custom-root">
131
+ <WheelPicker.Column aria-label="문자" options={options} className="custom-column" />
132
+ </WheelPicker.Root>,
133
+ );
134
+
135
+ expect(getByRole("group")).toHaveClass("custom-root");
136
+ expect(getByRole("spinbutton")).toHaveClass("custom-column");
137
+ });
138
+
139
+ it("renderLabel로 기본 ItemLabel을 대체한다", () => {
140
+ const { container, getByText } = render(
141
+ <WheelPicker.Root aria-label="휠 피커" itemSize={56}>
142
+ <WheelPicker.Column
143
+ aria-label="문자"
144
+ options={options}
145
+ renderLabel={(option) => <span data-custom-label="">{option.label}</span>}
146
+ />
147
+ </WheelPicker.Root>,
148
+ );
149
+
150
+ expect(getByText("A")).toHaveAttribute("data-custom-label");
151
+ expect(getByText("A").parentElement).toHaveClass("seed-wheel-picker-public__item");
152
+ expect(container.querySelector("[data-wheel-picker-item-label]")).not.toBeInTheDocument();
153
+ });
154
+
155
+ it("renderLabel에서 공개 ItemLabel을 재사용한다", () => {
156
+ const { getByText } = render(
157
+ <WheelPicker.Root aria-label="휠 피커">
158
+ <WheelPicker.Column
159
+ aria-label="문자"
160
+ options={options}
161
+ renderLabel={(option) => (
162
+ <WheelPicker.ItemLabel data-reused-label="">{option.label}</WheelPicker.ItemLabel>
163
+ )}
164
+ />
165
+ </WheelPicker.Root>,
166
+ );
167
+
168
+ expect(getByText("A")).toHaveClass("seed-wheel-picker-public__itemLabel");
169
+ expect(getByText("A")).toHaveAttribute("data-wheel-picker-item-label");
170
+ expect(getByText("A")).toHaveAttribute("data-reused-label");
171
+ });
172
+
173
+ it("option의 ariaLabel을 기본 접근성 텍스트로 사용한다", () => {
174
+ const { getByRole } = render(
175
+ <WheelPicker.Root aria-label="배지 선택">
176
+ <WheelPicker.Column
177
+ aria-label="배지"
178
+ options={[{ value: "popular", label: <span>◆ 인기</span>, ariaLabel: "인기" }]}
179
+ />
180
+ </WheelPicker.Root>,
181
+ );
182
+
183
+ expect(getByRole("spinbutton", { name: "배지" })).toHaveAttribute("aria-valuetext", "인기");
184
+ });
185
+
186
+ it("숫자 label을 기본 접근성 텍스트로 변환한다", () => {
187
+ const { getByRole } = render(
188
+ <WheelPicker.Root aria-label="수량 선택">
189
+ <WheelPicker.Column aria-label="수량" options={[{ value: "one", label: 1 }]} />
190
+ </WheelPicker.Root>,
191
+ );
192
+
193
+ expect(getByRole("spinbutton", { name: "수량" })).toHaveAttribute("aria-valuetext", "1");
194
+ });
195
+
196
+ it("Root의 disabled 상태를 Column과 Item에 전파한다", () => {
197
+ const { container, getByRole } = render(
198
+ <WheelPicker.Root aria-label="비활성 휠 피커" disabled>
199
+ <WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
200
+ </WheelPicker.Root>,
201
+ );
202
+
203
+ expect(getByRole("group")).toHaveAttribute("data-disabled");
204
+ expect(getByRole("spinbutton")).toHaveAttribute("data-disabled");
205
+ expect(getByRole("spinbutton")).toHaveAttribute("aria-disabled", "true");
206
+ expect(container.querySelector('[data-wheel-picker-value="a"]')).toHaveAttribute(
207
+ "data-selected",
208
+ );
209
+ });
210
+
211
+ it("지원하지 않는 readOnly 값이 런타임에 전달되어도 읽기 전용으로 동작하지 않는다", () => {
212
+ const RootWithLegacyReadOnly = WheelPicker.Root as React.ComponentType<
213
+ WheelPicker.RootProps & { readOnly?: boolean }
214
+ >;
215
+ const { getByRole } = render(
216
+ <RootWithLegacyReadOnly aria-label="휠 피커" readOnly>
217
+ <WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
218
+ </RootWithLegacyReadOnly>,
219
+ );
220
+
221
+ expect(getByRole("group")).not.toHaveAttribute("data-readonly");
222
+ expect(getByRole("spinbutton")).not.toHaveAttribute("aria-readonly");
223
+ });
224
+
225
+ it("BottomSheet 안에서 컬럼을 스크롤할 때 시트 드래그를 시작하지 않는다", () => {
226
+ const originalSetPointerCapture = HTMLElement.prototype.setPointerCapture;
227
+ HTMLElement.prototype.setPointerCapture = mock(() => {});
228
+
229
+ try {
230
+ const { container, getByRole } = render(
231
+ <BottomSheet.Root defaultOpen modal={false} autoFocus={false} skipAnimation>
232
+ <BottomSheet.Content>
233
+ <BottomSheet.Title>값 선택</BottomSheet.Title>
234
+ <BottomSheet.Description>원하는 값을 선택하세요.</BottomSheet.Description>
235
+ <WheelPicker.Root aria-label="휠 피커">
236
+ <WheelPicker.Column aria-label="문자" options={options} defaultValue="b" />
237
+ </WheelPicker.Root>
238
+ </BottomSheet.Content>
239
+ </BottomSheet.Root>,
240
+ );
241
+ const content = container.ownerDocument.querySelector("[data-drawer]") as HTMLElement;
242
+ const column = getByRole("spinbutton");
243
+
244
+ content.style.transform = "matrix(1, 0, 0, 1, 0, 0)";
245
+ spyOn(content, "getBoundingClientRect").mockReturnValue({
246
+ x: 0,
247
+ y: 0,
248
+ width: 320,
249
+ height: 400,
250
+ top: 0,
251
+ left: 0,
252
+ right: 320,
253
+ bottom: 400,
254
+ toJSON: () => {},
255
+ });
256
+ Object.defineProperties(column, {
257
+ clientHeight: { value: 220, configurable: true },
258
+ scrollHeight: { value: 440, configurable: true },
259
+ });
260
+ column.scrollTop = 44;
261
+
262
+ fireTouchPointer("pointerdown", column, 100);
263
+ fireTouchPointer("pointermove", column, 130);
264
+
265
+ expect(content.style.transform).toBe("matrix(1, 0, 0, 1, 0, 0)");
266
+ } finally {
267
+ HTMLElement.prototype.setPointerCapture = originalSetPointerCapture;
268
+ }
269
+ });
270
+ });
@@ -0,0 +1,210 @@
1
+ "use client";
2
+
3
+ import {
4
+ wheelPickerPublic,
5
+ type WheelPickerPublicVariantProps,
6
+ } from "@seed-design/css/recipes/wheel-picker-public";
7
+ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
8
+ import {
9
+ WheelPicker as WheelPickerPrimitive,
10
+ type WheelPickerOption,
11
+ } from "@seed-design/react-wheel-picker";
12
+ import clsx from "clsx";
13
+ import * as React from "react";
14
+ import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
15
+ import { ScrollFog } from "../ScrollFog/ScrollFog";
16
+
17
+ const DEFAULT_ITEM_SIZE = {
18
+ small: 36,
19
+ medium: 44,
20
+ } as const;
21
+ const DEFAULT_VISIBLE_ITEM_COUNT = 5;
22
+ const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(wheelPickerPublic);
23
+
24
+ type WheelPickerCssProperties = React.CSSProperties & {
25
+ "--seed-wheel-picker-public-item-size"?: string;
26
+ "--seed-wheel-picker-public-visible-item-count"?: number;
27
+ "--seed-wheel-picker-public-viewport-size"?: string;
28
+ "--seed-wheel-picker-public-center-offset"?: string;
29
+ "--seed-wheel-picker-public-scroll-fog-max-height"?: string;
30
+ };
31
+
32
+ export interface WheelPickerRootProps
33
+ extends WheelPickerPublicVariantProps,
34
+ Omit<
35
+ WheelPickerPrimitive.RootProps,
36
+ "asChild" | "children" | "disabled" | "itemSize" | "readOnly" | "visibleItemCount"
37
+ > {
38
+ /** Wheel Picker를 구성하는 `WheelPicker.Column` 목록입니다. */
39
+ children: React.ReactNode;
40
+
41
+ /** 모든 컬럼의 포커스와 값 변경을 막습니다. */
42
+ disabled?: boolean;
43
+
44
+ /**
45
+ * 한 항목의 높이입니다. 지정하지 않으면 `size`의 기본 높이를 사용합니다.
46
+ */
47
+ itemSize?: number;
48
+
49
+ /**
50
+ * 화면에 보이는 항목 수입니다. 5 이상의 홀수를 권장합니다.
51
+ * @default 5
52
+ */
53
+ visibleItemCount?: number;
54
+ }
55
+
56
+ export const WheelPickerRoot = React.forwardRef<HTMLDivElement, WheelPickerRootProps>(
57
+ (props, ref) => {
58
+ const [variantProps, otherProps] = wheelPickerPublic.splitVariantProps(props);
59
+ const {
60
+ children,
61
+ className,
62
+ itemSize: itemSizeProp,
63
+ style,
64
+ visibleItemCount = DEFAULT_VISIBLE_ITEM_COUNT,
65
+ ...rootProps
66
+ } = otherProps;
67
+ const size = variantProps.size ?? "medium";
68
+ const itemSize = itemSizeProp ?? DEFAULT_ITEM_SIZE[size];
69
+ const centerOffset = ((visibleItemCount - 1) / 2) * itemSize;
70
+ const classNames = wheelPickerPublic({ size });
71
+ const wheelPickerStyle: WheelPickerCssProperties = {
72
+ ...style,
73
+ "--seed-wheel-picker-public-item-size": `${itemSize}px`,
74
+ "--seed-wheel-picker-public-visible-item-count": visibleItemCount,
75
+ "--seed-wheel-picker-public-viewport-size": `${itemSize * visibleItemCount}px`,
76
+ "--seed-wheel-picker-public-center-offset": `${centerOffset}px`,
77
+ "--seed-wheel-picker-public-scroll-fog-max-height": `${itemSize * 3}px`,
78
+ };
79
+
80
+ return (
81
+ <ClassNamesProvider value={classNames}>
82
+ <WheelPickerPrimitive.Root
83
+ ref={ref}
84
+ className={clsx(classNames.root, className)}
85
+ itemSize={itemSize}
86
+ style={wheelPickerStyle}
87
+ visibleItemCount={visibleItemCount}
88
+ {...rootProps}
89
+ readOnly={false}
90
+ >
91
+ <Primitive.div
92
+ aria-hidden
93
+ className={classNames.selectionIndicator}
94
+ data-wheel-picker-indicator=""
95
+ />
96
+ <ScrollFog
97
+ className={classNames.scrollFog}
98
+ placement={["top", "bottom"]}
99
+ size="var(--seed-wheel-picker-public-scroll-fog-size)"
100
+ hideScrollBar
101
+ data-wheel-picker-scroll-fog=""
102
+ >
103
+ <Primitive.div className={classNames.columns} data-wheel-picker-columns="">
104
+ {children}
105
+ </Primitive.div>
106
+ </ScrollFog>
107
+ </WheelPickerPrimitive.Root>
108
+ </ClassNamesProvider>
109
+ );
110
+ },
111
+ );
112
+ WheelPickerRoot.displayName = "WheelPickerRoot";
113
+
114
+ export interface WheelPickerItemLabelProps
115
+ extends PrimitiveProps,
116
+ React.HTMLAttributes<HTMLDivElement> {}
117
+
118
+ /** Wheel Picker 항목의 기본 여백과 타이포그래피를 적용합니다. */
119
+ export const WheelPickerItemLabel = React.forwardRef<HTMLDivElement, WheelPickerItemLabelProps>(
120
+ ({ className, ...props }, ref) => {
121
+ const classNames = useClassNames();
122
+
123
+ return (
124
+ <Primitive.div
125
+ ref={ref}
126
+ className={clsx(classNames.itemLabel, className)}
127
+ {...props}
128
+ data-wheel-picker-item-label=""
129
+ />
130
+ );
131
+ },
132
+ );
133
+ WheelPickerItemLabel.displayName = "WheelPickerItemLabel";
134
+
135
+ export interface WheelPickerColumnProps
136
+ extends Omit<
137
+ WheelPickerPrimitive.ColumnProps,
138
+ | "asChild"
139
+ | "defaultValue"
140
+ | "getAriaValueText"
141
+ | "loop"
142
+ | "onIndexChange"
143
+ | "onValueChange"
144
+ | "options"
145
+ | "renderOption"
146
+ | "value"
147
+ | "valueChangeBehavior"
148
+ > {
149
+ /** 컬럼에 표시할 선택 항목입니다. 하나 이상의 항목을 제공해야 합니다. */
150
+ options: readonly WheelPickerOption[];
151
+
152
+ /** 제어 상태에서 현재 선택된 항목의 값입니다. */
153
+ value?: string;
154
+
155
+ /** 비제어 상태에서 처음 선택할 항목의 값입니다. */
156
+ defaultValue?: string;
157
+
158
+ /** 선택 값이 바뀔 때 호출됩니다. */
159
+ onValueChange?: WheelPickerPrimitive.ColumnProps["onValueChange"];
160
+
161
+ /** 사용자 조작으로 중앙 항목이 바뀔 때마다 호출됩니다. */
162
+ onIndexChange?: WheelPickerPrimitive.ColumnProps["onIndexChange"];
163
+
164
+ /**
165
+ * 마지막 항목과 첫 항목을 이어 반복해서 탐색할지 여부입니다.
166
+ * @default false
167
+ */
168
+ loop?: boolean;
169
+
170
+ /**
171
+ * 외부에서 `value`가 변경되었을 때 새 값으로 이동하는 스크롤 방식입니다.
172
+ * @default "auto"
173
+ */
174
+ valueChangeBehavior?: ScrollBehavior;
175
+
176
+ /** 현재 값에 대응하는 접근성 텍스트를 반환합니다. */
177
+ getAriaValueText?: (value: string) => string;
178
+
179
+ /**
180
+ * 기본 `ItemLabel` 대신 항목에 표시할 요소를 반환합니다.
181
+ * 반환한 요소는 항목의 선택·비활성 색상을 적용할 수 있도록 `currentColor`를 상속해야 합니다.
182
+ */
183
+ renderLabel?: (option: WheelPickerOption) => React.ReactNode;
184
+ }
185
+
186
+ export const WheelPickerColumn = React.forwardRef<HTMLDivElement, WheelPickerColumnProps>(
187
+ ({ className, renderLabel, ...props }, ref) => {
188
+ const classNames = useClassNames();
189
+
190
+ return (
191
+ <WheelPickerPrimitive.Column
192
+ ref={ref}
193
+ className={clsx(classNames.column, className)}
194
+ {...props}
195
+ renderOption={(option, optionProps) => (
196
+ <Primitive.div className={classNames.item} {...optionProps}>
197
+ {renderLabel ? (
198
+ renderLabel(option)
199
+ ) : (
200
+ <WheelPickerItemLabel>{option.label}</WheelPickerItemLabel>
201
+ )}
202
+ </Primitive.div>
203
+ )}
204
+ />
205
+ );
206
+ },
207
+ );
208
+ WheelPickerColumn.displayName = "WheelPickerColumn";
209
+
210
+ export type { WheelPickerOption };
@@ -0,0 +1,11 @@
1
+ export {
2
+ WheelPickerRoot,
3
+ WheelPickerColumn,
4
+ WheelPickerItemLabel,
5
+ type WheelPickerRootProps,
6
+ type WheelPickerColumnProps,
7
+ type WheelPickerItemLabelProps,
8
+ type WheelPickerOption,
9
+ } from "./WheelPicker";
10
+
11
+ export * as WheelPicker from "./WheelPicker.namespace";
@@ -84,3 +84,4 @@ export * from "./TextField";
84
84
  export * from "./TimePicker";
85
85
  export * from "./ToggleButton";
86
86
  export * from "./VisuallyHidden";
87
+ export * from "./WheelPicker";
@@ -49,6 +49,7 @@ describe("InternalWheelPicker", () => {
49
49
  expect(columns).toHaveClass("seed-wheel-picker__columns", "columns");
50
50
  expect(getAllByRole("spinbutton")).toHaveLength(2);
51
51
  expect(firstColumn).toHaveClass("seed-wheel-picker__column");
52
+ expect(firstColumn).not.toHaveClass("seed-wheel-picker-public__column");
52
53
  expect(firstColumn.firstElementChild).toHaveClass("seed-wheel-picker__item");
53
54
  });
54
55
  });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { wheelPicker } from "@seed-design/css/recipes/wheel-picker";
4
4
  import { Primitive } from "@seed-design/react-primitive";
5
- import * as HeadlessWheelPicker from "@seed-design/react-wheel-picker";
5
+ import { WheelPicker as WheelPickerPrimitive } from "@seed-design/react-wheel-picker";
6
6
  import clsx from "clsx";
7
7
  import * as React from "react";
8
8
  import { ScrollFog, type ScrollFogProps } from "../ScrollFog/ScrollFog";
@@ -17,7 +17,7 @@ type WheelPickerCssProperties = React.CSSProperties & {
17
17
  };
18
18
 
19
19
  export interface InternalWheelPickerRootProps
20
- extends Omit<HeadlessWheelPicker.WheelPickerRootProps, "asChild" | "children"> {
20
+ extends Omit<WheelPickerPrimitive.RootProps, "asChild" | "children"> {
21
21
  children: React.ReactNode;
22
22
  columnsClassName?: string;
23
23
  scrollFogClassName?: string;
@@ -54,7 +54,7 @@ export const InternalWheelPickerRoot = React.forwardRef<
54
54
  };
55
55
 
56
56
  return (
57
- <HeadlessWheelPicker.WheelPickerRoot
57
+ <WheelPickerPrimitive.Root
58
58
  ref={ref}
59
59
  itemSize={itemSize}
60
60
  visibleItemCount={visibleItemCount}
@@ -81,14 +81,14 @@ export const InternalWheelPickerRoot = React.forwardRef<
81
81
  {children}
82
82
  </Primitive.div>
83
83
  </ScrollFog>
84
- </HeadlessWheelPicker.WheelPickerRoot>
84
+ </WheelPickerPrimitive.Root>
85
85
  );
86
86
  },
87
87
  );
88
88
  InternalWheelPickerRoot.displayName = "InternalWheelPickerRoot";
89
89
 
90
90
  export interface InternalWheelPickerColumnProps
91
- extends Omit<HeadlessWheelPicker.WheelPickerColumnProps, "asChild" | "renderOption"> {
91
+ extends Omit<WheelPickerPrimitive.ColumnProps, "asChild" | "renderOption"> {
92
92
  itemClassName?: string;
93
93
  }
94
94
 
@@ -96,7 +96,7 @@ export const InternalWheelPickerColumn = React.forwardRef<
96
96
  HTMLDivElement,
97
97
  InternalWheelPickerColumnProps
98
98
  >(({ className, itemClassName, ...props }, ref) => (
99
- <HeadlessWheelPicker.WheelPickerColumn
99
+ <WheelPickerPrimitive.Column
100
100
  ref={ref}
101
101
  className={clsx(classNames.column, className)}
102
102
  renderOption={(option, optionProps) => (
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@ export type { ResponsiveValue, UnwrapResponsive, BreakpointThreshold } from "./t
5
5
  export { useBreakpoint } from "./hooks/useBreakpoint";
6
6
  export type { UseBreakpointOptions } from "./hooks/useBreakpoint";
7
7
  export { useBreakpointValue } from "./hooks/useBreakpointValue";
8
+ export { useScaleFeedback, ScaleFeedback } from "@seed-design/react-scale-feedback";
9
+ export type { ScaleFeedbackProps } from "@seed-design/react-scale-feedback";
8
10
  export { BreakpointProvider } from "./providers/BreakpointProvider";
9
11
  export type { BreakpointProviderProps } from "./providers/BreakpointProvider";
10
12
 
@@ -0,0 +1,34 @@
1
+ import { useComposedRefs } from "@radix-ui/react-compose-refs";
2
+ import { useScaleFeedback } from "@seed-design/react-scale-feedback";
3
+ import clsx from "clsx";
4
+ import * as React from "react";
5
+
6
+ /**
7
+ * Wraps a component so its element opts into scale feedback — for one-liner
8
+ * factory components (`withContext`/`withProvider` results). Hand-written
9
+ * forwardRef components call `useScaleFeedback` directly instead.
10
+ */
11
+ export function withScaleFeedback<P>(
12
+ Component: React.ForwardRefExoticComponent<P>,
13
+ ): React.ForwardRefExoticComponent<P>;
14
+ export function withScaleFeedback(
15
+ Component: React.ForwardRefExoticComponent<
16
+ React.RefAttributes<HTMLElement> & { className?: string }
17
+ >,
18
+ ): React.ForwardRefExoticComponent<React.RefAttributes<HTMLElement> & { className?: string }> {
19
+ const Pressable = React.forwardRef<HTMLElement, { className?: string }>((props, ref) => {
20
+ const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
21
+ const composedRef = useComposedRefs(scaleFeedbackRef, ref);
22
+
23
+ return (
24
+ <Component
25
+ {...props}
26
+ ref={composedRef}
27
+ className={clsx(scaleFeedbackClassName, props.className)}
28
+ />
29
+ );
30
+ });
31
+
32
+ Pressable.displayName = Component.displayName;
33
+ return Pressable;
34
+ }