@seed-design/lynx-react 0.7.0 → 0.8.0

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 (245) hide show
  1. package/lib/components/Accordion/Accordion.jsx +2 -2
  2. package/lib/components/ActionButton/ActionButton.jsx +1 -1
  3. package/lib/components/AlertDialog/AlertDialog.d.ts +64 -0
  4. package/lib/components/AlertDialog/AlertDialog.d.ts.map +1 -0
  5. package/lib/components/AlertDialog/AlertDialog.jsx +138 -0
  6. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts +2 -0
  7. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts.map +1 -0
  8. package/lib/components/AlertDialog/AlertDialog.namespace.js +17 -0
  9. package/lib/components/AlertDialog/index.d.ts +3 -0
  10. package/lib/components/AlertDialog/index.d.ts.map +1 -0
  11. package/lib/components/AppBar/AppBar.d.ts +14 -1
  12. package/lib/components/AppBar/AppBar.d.ts.map +1 -1
  13. package/lib/components/AppBar/AppBar.jsx +46 -10
  14. package/lib/components/AppBar/AppBar.namespace.d.ts +1 -1
  15. package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -1
  16. package/lib/components/AppBar/index.d.ts +1 -1
  17. package/lib/components/AppBar/index.d.ts.map +1 -1
  18. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts +182 -0
  19. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts.map +1 -0
  20. package/lib/components/AttachmentDisplay/AttachmentDisplay.jsx +444 -0
  21. package/lib/components/AttachmentDisplay/index.d.ts +2 -0
  22. package/lib/components/AttachmentDisplay/index.d.ts.map +1 -0
  23. package/lib/components/AttachmentInput/AttachmentInput.d.ts +179 -0
  24. package/lib/components/AttachmentInput/AttachmentInput.d.ts.map +1 -0
  25. package/lib/components/AttachmentInput/AttachmentInput.jsx +520 -0
  26. package/lib/components/AttachmentInput/index.d.ts +2 -0
  27. package/lib/components/AttachmentInput/index.d.ts.map +1 -0
  28. package/lib/components/BottomSheet/BottomSheet.d.ts +1 -1
  29. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  30. package/lib/components/BottomSheet/BottomSheet.jsx +1 -7
  31. package/lib/components/Callout/Callout.jsx +2 -2
  32. package/lib/components/Checkbox/Checkbox.jsx +2 -2
  33. package/lib/components/Chip/Chip.jsx +2 -2
  34. package/lib/components/ChipTabs/ChipTabs.d.ts +41 -0
  35. package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -0
  36. package/lib/components/ChipTabs/ChipTabs.jsx +39 -0
  37. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts +2 -0
  38. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts.map +1 -0
  39. package/lib/components/ChipTabs/ChipTabs.namespace.js +13 -0
  40. package/lib/components/ChipTabs/index.d.ts +3 -0
  41. package/lib/components/ChipTabs/index.d.ts.map +1 -0
  42. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts +18 -0
  43. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -0
  44. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.jsx +113 -0
  45. package/lib/components/ContextualFloatingButton/index.d.ts +2 -0
  46. package/lib/components/ContextualFloatingButton/index.d.ts.map +1 -0
  47. package/lib/components/Dialog/Dialog.d.ts +87 -0
  48. package/lib/components/Dialog/Dialog.d.ts.map +1 -0
  49. package/lib/components/Dialog/Dialog.jsx +147 -0
  50. package/lib/components/Dialog/Dialog.namespace.d.ts +2 -0
  51. package/lib/components/Dialog/Dialog.namespace.d.ts.map +1 -0
  52. package/lib/components/Dialog/Dialog.namespace.js +18 -0
  53. package/lib/components/Dialog/index.d.ts +3 -0
  54. package/lib/components/Dialog/index.d.ts.map +1 -0
  55. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts +23 -0
  56. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -0
  57. package/lib/components/FloatingActionButton/FloatingActionButton.jsx +76 -0
  58. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts +2 -0
  59. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts.map +1 -0
  60. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.js +10 -0
  61. package/lib/components/FloatingActionButton/index.d.ts +3 -0
  62. package/lib/components/FloatingActionButton/index.d.ts.map +1 -0
  63. package/lib/components/HelpBubble/HelpBubble.d.ts +67 -0
  64. package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -0
  65. package/lib/components/HelpBubble/HelpBubble.jsx +600 -0
  66. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts +2 -0
  67. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts.map +1 -0
  68. package/lib/components/HelpBubble/HelpBubble.namespace.js +18 -0
  69. package/lib/components/HelpBubble/index.d.ts +3 -0
  70. package/lib/components/HelpBubble/index.d.ts.map +1 -0
  71. package/lib/components/Icon/Icon.d.ts +1 -0
  72. package/lib/components/Icon/Icon.d.ts.map +1 -1
  73. package/lib/components/Icon/Icon.jsx +3 -2
  74. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts +28 -0
  75. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts.map +1 -0
  76. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.jsx +37 -0
  77. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts +2 -0
  78. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts.map +1 -0
  79. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.js +9 -0
  80. package/lib/components/IdentityPlaceholder/identity-placeholder-business.js +4 -0
  81. package/lib/components/IdentityPlaceholder/identity-placeholder-person.js +4 -0
  82. package/lib/components/IdentityPlaceholder/index.d.ts +3 -0
  83. package/lib/components/IdentityPlaceholder/index.d.ts.map +1 -0
  84. package/lib/components/InputButton/InputButton.d.ts +2 -0
  85. package/lib/components/InputButton/InputButton.d.ts.map +1 -1
  86. package/lib/components/InputButton/InputButton.jsx +40 -8
  87. package/lib/components/List/List.d.ts +1 -1
  88. package/lib/components/List/List.d.ts.map +1 -1
  89. package/lib/components/List/List.jsx +26 -12
  90. package/lib/components/Menu/Menu.d.ts.map +1 -1
  91. package/lib/components/Menu/Menu.jsx +15 -5
  92. package/lib/components/PageBanner/PageBanner.d.ts +3 -0
  93. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
  94. package/lib/components/PageBanner/PageBanner.jsx +49 -11
  95. package/lib/components/QuantityPicker/QuantityPicker.d.ts +53 -0
  96. package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -0
  97. package/lib/components/QuantityPicker/QuantityPicker.jsx +282 -0
  98. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts +2 -0
  99. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts.map +1 -0
  100. package/lib/components/QuantityPicker/QuantityPicker.namespace.js +11 -0
  101. package/lib/components/QuantityPicker/index.d.ts +3 -0
  102. package/lib/components/QuantityPicker/index.d.ts.map +1 -0
  103. package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
  104. package/lib/components/ReactionButton/ReactionButton.d.ts +1 -1
  105. package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
  106. package/lib/components/ReactionButton/ReactionButton.jsx +22 -13
  107. package/lib/components/ScrollFog/ScrollFog.d.ts +41 -0
  108. package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -0
  109. package/lib/components/ScrollFog/ScrollFog.jsx +65 -0
  110. package/lib/components/ScrollFog/index.d.ts +2 -0
  111. package/lib/components/ScrollFog/index.d.ts.map +1 -0
  112. package/lib/components/SegmentedControl/SegmentedControl.jsx +2 -2
  113. package/lib/components/Select/Select.d.ts +112 -0
  114. package/lib/components/Select/Select.d.ts.map +1 -0
  115. package/lib/components/Select/Select.jsx +885 -0
  116. package/lib/components/Select/Select.namespace.d.ts +2 -0
  117. package/lib/components/Select/Select.namespace.d.ts.map +1 -0
  118. package/lib/components/Select/Select.namespace.js +23 -0
  119. package/lib/components/Select/index.d.ts +3 -0
  120. package/lib/components/Select/index.d.ts.map +1 -0
  121. package/lib/components/Skeleton/Skeleton.d.ts +15 -0
  122. package/lib/components/Skeleton/Skeleton.d.ts.map +1 -0
  123. package/lib/components/Skeleton/Skeleton.jsx +18 -0
  124. package/lib/components/Skeleton/index.d.ts +2 -0
  125. package/lib/components/Skeleton/index.d.ts.map +1 -0
  126. package/lib/components/Slider/Slider.d.ts +101 -0
  127. package/lib/components/Slider/Slider.d.ts.map +1 -0
  128. package/lib/components/Slider/Slider.jsx +573 -0
  129. package/lib/components/Slider/Slider.namespace.d.ts +2 -0
  130. package/lib/components/Slider/Slider.namespace.d.ts.map +1 -0
  131. package/lib/components/Slider/Slider.namespace.js +19 -0
  132. package/lib/components/Slider/Slider.utils.d.ts +19 -0
  133. package/lib/components/Slider/Slider.utils.d.ts.map +1 -0
  134. package/lib/components/Slider/Slider.utils.js +106 -0
  135. package/lib/components/Slider/index.d.ts +3 -0
  136. package/lib/components/Slider/index.d.ts.map +1 -0
  137. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
  138. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +22 -8
  139. package/lib/components/Switch/Switch.d.ts.map +1 -1
  140. package/lib/components/Switch/Switch.jsx +5 -3
  141. package/lib/components/Tabs/Tabs.d.ts +34 -2
  142. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  143. package/lib/components/Tabs/Tabs.jsx +186 -56
  144. package/lib/components/Tabs/Tabs.utils.d.ts +14 -1
  145. package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
  146. package/lib/components/Tabs/Tabs.utils.js +24 -3
  147. package/lib/components/Tabs/index.d.ts +1 -1
  148. package/lib/components/Tabs/index.d.ts.map +1 -1
  149. package/lib/components/index.d.ts +14 -0
  150. package/lib/components/index.d.ts.map +1 -1
  151. package/lib/hooks/useControllableState.d.ts +1 -14
  152. package/lib/hooks/useControllableState.d.ts.map +1 -1
  153. package/lib/hooks/usePressTap.d.ts +1 -27
  154. package/lib/hooks/usePressTap.d.ts.map +1 -1
  155. package/lib/hooks/useScaleFeedback.d.ts +1 -1
  156. package/lib/hooks/useScaleFeedback.js +1 -1
  157. package/lib/index.js +29 -6
  158. package/lib/{hooks/useControllableState.js → packages/lynx-react-headless/use-controllable-state/lib/index.js} +2 -3
  159. package/lib/{hooks/usePressTap.js → packages/lynx-react-headless/use-press-tap/lib/index.js} +13 -10
  160. package/lib/types.d.ts +1 -0
  161. package/lib/types.d.ts.map +1 -1
  162. package/package.json +9 -5
  163. package/src/components/ActionButton/ActionButton.test.tsx +7 -7
  164. package/src/components/AlertDialog/AlertDialog.namespace.ts +22 -0
  165. package/src/components/AlertDialog/AlertDialog.test.tsx +226 -0
  166. package/src/components/AlertDialog/AlertDialog.tsx +374 -0
  167. package/src/components/AlertDialog/index.ts +24 -0
  168. package/src/components/AppBar/AppBar.namespace.ts +1 -0
  169. package/src/components/AppBar/AppBar.test.tsx +257 -0
  170. package/src/components/AppBar/AppBar.tsx +109 -14
  171. package/src/components/AppBar/index.ts +1 -0
  172. package/src/components/AttachmentDisplay/AttachmentDisplay.tsx +886 -0
  173. package/src/components/AttachmentDisplay/index.ts +48 -0
  174. package/src/components/AttachmentInput/AttachmentInput.tsx +1001 -0
  175. package/src/components/AttachmentInput/index.ts +49 -0
  176. package/src/components/BottomSheet/BottomSheet.test.tsx +22 -7
  177. package/src/components/BottomSheet/BottomSheet.tsx +1 -19
  178. package/src/components/ChipTabs/ChipTabs.namespace.ts +14 -0
  179. package/src/components/ChipTabs/ChipTabs.tsx +119 -0
  180. package/src/components/ChipTabs/index.ts +16 -0
  181. package/src/components/ContextualFloatingButton/ContextualFloatingButton.test.tsx +63 -0
  182. package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +232 -0
  183. package/src/components/ContextualFloatingButton/index.ts +4 -0
  184. package/src/components/Dialog/Dialog.namespace.ts +24 -0
  185. package/src/components/Dialog/Dialog.test.tsx +172 -0
  186. package/src/components/Dialog/Dialog.tsx +373 -0
  187. package/src/components/Dialog/index.ts +26 -0
  188. package/src/components/FloatingActionButton/FloatingActionButton.namespace.ts +8 -0
  189. package/src/components/FloatingActionButton/FloatingActionButton.test.tsx +59 -0
  190. package/src/components/FloatingActionButton/FloatingActionButton.tsx +207 -0
  191. package/src/components/FloatingActionButton/index.ts +10 -0
  192. package/src/components/HelpBubble/HelpBubble.namespace.ts +24 -0
  193. package/src/components/HelpBubble/HelpBubble.tsx +998 -0
  194. package/src/components/HelpBubble/index.ts +26 -0
  195. package/src/components/Icon/Icon.test.tsx +57 -5
  196. package/src/components/Icon/Icon.tsx +16 -2
  197. package/src/components/IdentityPlaceholder/IdentityPlaceholder.namespace.ts +6 -0
  198. package/src/components/IdentityPlaceholder/IdentityPlaceholder.tsx +103 -0
  199. package/src/components/IdentityPlaceholder/assets.d.ts +4 -0
  200. package/src/components/IdentityPlaceholder/identity-placeholder-business.webp +0 -0
  201. package/src/components/IdentityPlaceholder/identity-placeholder-person.webp +0 -0
  202. package/src/components/IdentityPlaceholder/index.ts +8 -0
  203. package/src/components/InputButton/InputButton.test.tsx +128 -1
  204. package/src/components/InputButton/InputButton.tsx +60 -9
  205. package/src/components/List/List.test.tsx +54 -2
  206. package/src/components/List/List.tsx +43 -10
  207. package/src/components/Menu/Menu.tsx +14 -4
  208. package/src/components/Menu/__tests__/Menu.test.tsx +34 -0
  209. package/src/components/PageBanner/PageBanner.test.tsx +110 -3
  210. package/src/components/PageBanner/PageBanner.tsx +83 -19
  211. package/src/components/QuantityPicker/QuantityPicker.namespace.ts +10 -0
  212. package/src/components/QuantityPicker/QuantityPicker.test.tsx +235 -0
  213. package/src/components/QuantityPicker/QuantityPicker.tsx +605 -0
  214. package/src/components/QuantityPicker/index.ts +14 -0
  215. package/src/components/ReactionButton/ReactionButton.feedback.test.tsx +50 -0
  216. package/src/components/ReactionButton/ReactionButton.test.tsx +6 -4
  217. package/src/components/ReactionButton/ReactionButton.tsx +20 -12
  218. package/src/components/ScrollFog/ScrollFog.test.tsx +222 -0
  219. package/src/components/ScrollFog/ScrollFog.tsx +172 -0
  220. package/src/components/ScrollFog/index.ts +1 -0
  221. package/src/components/Select/Select.namespace.ts +34 -0
  222. package/src/components/Select/Select.test.tsx +297 -0
  223. package/src/components/Select/Select.tsx +1483 -0
  224. package/src/components/Select/index.ts +39 -0
  225. package/src/components/Skeleton/Skeleton.test.tsx +64 -0
  226. package/src/components/Skeleton/Skeleton.tsx +45 -0
  227. package/src/components/Skeleton/index.ts +1 -0
  228. package/src/components/Slider/Slider.namespace.ts +26 -0
  229. package/src/components/Slider/Slider.test.tsx +260 -0
  230. package/src/components/Slider/Slider.tsx +897 -0
  231. package/src/components/Slider/Slider.utils.ts +167 -0
  232. package/src/components/Slider/index.ts +30 -0
  233. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +21 -1
  234. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +30 -7
  235. package/src/components/Switch/Switch.tsx +14 -13
  236. package/src/components/Tabs/Tabs.test.tsx +218 -1
  237. package/src/components/Tabs/Tabs.tsx +346 -75
  238. package/src/components/Tabs/Tabs.utils.ts +51 -1
  239. package/src/components/Tabs/index.ts +17 -1
  240. package/src/components/index.ts +14 -0
  241. package/src/hooks/useControllableState.ts +1 -43
  242. package/src/hooks/usePressTap.test.ts +23 -11
  243. package/src/hooks/usePressTap.ts +1 -60
  244. package/src/hooks/useScaleFeedback.ts +1 -1
  245. package/src/types.ts +1 -0
@@ -0,0 +1,605 @@
1
+ import {
2
+ quantityPicker,
3
+ type QuantityPickerSlotName,
4
+ type QuantityPickerVariantProps,
5
+ } from "@seed-design/lynx-css/recipes/quantity-picker";
6
+ import clsx from "clsx";
7
+ import * as React from "@lynx-js/react";
8
+
9
+ import { useControllableState } from "../../hooks/useControllableState";
10
+ import { usePressTap } from "../../hooks/usePressTap";
11
+ import { useScaleFeedback } from "../../hooks/useScaleFeedback";
12
+ import type {
13
+ LynxAccessibilityProps,
14
+ LynxIconElementProps,
15
+ LynxStyledElementProps,
16
+ LynxTouchProps,
17
+ LynxViewRef,
18
+ } from "../../types";
19
+ import { mergeProps } from "../../utils/merge-props";
20
+ import { InternalIcon } from "../Icon/Icon";
21
+
22
+ export type QuantityPickerLoading =
23
+ | boolean
24
+ | {
25
+ decrement?: boolean;
26
+ increment?: boolean;
27
+ };
28
+
29
+ export type QuantityPickerGetValueText = (
30
+ valueText: string,
31
+ value: number | string,
32
+ ) => React.ReactNode;
33
+
34
+ const defaultGetValueText: QuantityPickerGetValueText = (valueText) => valueText;
35
+
36
+ type QuantityPickerRootBaseProps = Omit<LynxStyledElementProps, "children"> &
37
+ LynxAccessibilityProps & {
38
+ children?: React.ReactNode;
39
+ min: number;
40
+ max: number;
41
+ step?: number;
42
+ value?: number;
43
+ defaultValue?: number;
44
+ onValueChange?: (value: number) => void;
45
+ disabled?: boolean;
46
+ invalid?: boolean;
47
+ readOnly?: boolean;
48
+ loading?: QuantityPickerLoading;
49
+ onRemove?: () => void;
50
+ getValueText?: QuantityPickerGetValueText;
51
+ dir?: "ltr" | "rtl";
52
+ layout?: QuantityPickerVariantProps["layout"];
53
+ size?: QuantityPickerVariantProps["size"];
54
+ };
55
+
56
+ type QuantityPickerRemovableProps = {
57
+ removable: true;
58
+ removeAccessibilityLabel: string;
59
+ onRemove: () => void;
60
+ };
61
+
62
+ type QuantityPickerNonRemovableProps = {
63
+ removable?: false;
64
+ removeAccessibilityLabel?: string;
65
+ };
66
+
67
+ export type QuantityPickerRootProps = QuantityPickerRootBaseProps &
68
+ (QuantityPickerRemovableProps | QuantityPickerNonRemovableProps);
69
+
70
+ export interface QuantityPickerDecrementButtonProps
71
+ extends LynxStyledElementProps,
72
+ LynxTouchProps,
73
+ LynxAccessibilityProps {
74
+ icon?: React.ReactNode;
75
+ loadingIndicator?: React.ReactNode;
76
+ removeIcon?: React.ReactNode;
77
+ }
78
+
79
+ export interface QuantityPickerValueDisplayProps
80
+ extends Omit<LynxStyledElementProps, "children">,
81
+ LynxAccessibilityProps {}
82
+
83
+ export interface QuantityPickerIncrementButtonProps
84
+ extends LynxStyledElementProps,
85
+ LynxTouchProps,
86
+ LynxAccessibilityProps {
87
+ icon?: React.ReactNode;
88
+ loadingIndicator?: React.ReactNode;
89
+ }
90
+
91
+ type QuantityPickerClassNames = Record<QuantityPickerSlotName, string>;
92
+
93
+ interface QuantityPickerContextValue {
94
+ layout: QuantityPickerVariantProps["layout"];
95
+ size: QuantityPickerVariantProps["size"];
96
+ invalid: boolean;
97
+ disabled: boolean;
98
+ readOnly: boolean;
99
+ loading: boolean;
100
+ value: number;
101
+ min: number;
102
+ max: number;
103
+ step: number;
104
+ dir: "ltr" | "rtl";
105
+ removable: boolean;
106
+ isAtMin: boolean;
107
+ isAtMax: boolean;
108
+ isRemoveButton: boolean;
109
+ removeAccessibilityLabel?: string;
110
+ getValueText: QuantityPickerGetValueText;
111
+ decrementLoading: boolean;
112
+ incrementLoading: boolean;
113
+ decrementBlocked: boolean;
114
+ incrementBlocked: boolean;
115
+ decrement: () => void;
116
+ increment: () => void;
117
+ onRemove?: () => void;
118
+ classes: QuantityPickerClassNames;
119
+ }
120
+
121
+ const QuantityPickerContext = React.createContext<QuantityPickerContextValue | null>(null);
122
+
123
+ function useQuantityPickerContext(consumer: string): QuantityPickerContextValue {
124
+ const context = React.useContext(QuantityPickerContext);
125
+ if (!context) {
126
+ throw new Error(`<${consumer}/> must be rendered inside <QuantityPickerRoot/>.`);
127
+ }
128
+ return context;
129
+ }
130
+
131
+ function assertSafeInteger(value: number | undefined, name: string): asserts value is number {
132
+ if (!Number.isSafeInteger(value)) {
133
+ throw new Error(`QuantityPicker: ${name} must be a safe integer.`);
134
+ }
135
+ }
136
+
137
+ function validateProps({
138
+ defaultValue,
139
+ max,
140
+ min,
141
+ step,
142
+ value,
143
+ }: Pick<QuantityPickerRootProps, "defaultValue" | "max" | "min" | "step" | "value">) {
144
+ assertSafeInteger(min, "min");
145
+ assertSafeInteger(max, "max");
146
+ assertSafeInteger(step, "step");
147
+
148
+ if (min > max) {
149
+ throw new Error("QuantityPicker: min must be less than or equal to max.");
150
+ }
151
+ if (step <= 0) {
152
+ throw new Error("QuantityPicker: step must be greater than 0.");
153
+ }
154
+
155
+ for (const [name, candidate] of [
156
+ ["value", value],
157
+ ["defaultValue", defaultValue],
158
+ ] as const) {
159
+ if (candidate === undefined) continue;
160
+ assertSafeInteger(candidate, name);
161
+ if (candidate < min || candidate > max) {
162
+ throw new Error(`QuantityPicker: ${name} must be between min and max.`);
163
+ }
164
+ }
165
+ }
166
+
167
+ function getLoadingState(loading: QuantityPickerLoading | undefined) {
168
+ if (loading === true) return { decrement: true, increment: true };
169
+ return {
170
+ decrement: typeof loading === "object" ? (loading?.decrement ?? false) : false,
171
+ increment: typeof loading === "object" ? (loading?.increment ?? false) : false,
172
+ };
173
+ }
174
+
175
+ function getValueDisplayPlaceholder(min: number, max: number) {
176
+ const boundary = String(min).length > String(max).length ? min : max;
177
+ return String(boundary).replace(/\d/g, "0");
178
+ }
179
+
180
+ function getAccessibleText(valueText: React.ReactNode, value: number) {
181
+ return typeof valueText === "string" || typeof valueText === "number"
182
+ ? String(valueText)
183
+ : String(value);
184
+ }
185
+
186
+ function recipeProps(
187
+ context: Pick<QuantityPickerContextValue, "layout" | "size" | "invalid" | "disabled"> &
188
+ Partial<Pick<QuantityPickerContextValue, "loading">>,
189
+ pressed = false,
190
+ ) {
191
+ return {
192
+ layout: context.layout,
193
+ size: context.size,
194
+ invalid: context.invalid,
195
+ disabled: context.disabled,
196
+ pressed,
197
+ loading: context.loading ?? false,
198
+ } as QuantityPickerVariantProps;
199
+ }
200
+
201
+ function isQuantityPickerButton(child: React.ReactNode) {
202
+ return (
203
+ React.isValidElement(child) &&
204
+ (child.type === QuantityPickerDecrementButton || child.type === QuantityPickerIncrementButton)
205
+ );
206
+ }
207
+
208
+ function isQuantityPickerValueDisplay(child: React.ReactNode) {
209
+ return React.isValidElement(child) && child.type === QuantityPickerValueDisplay;
210
+ }
211
+ function flattenQuantityPickerChildren(children: React.ReactNode): React.ReactNode[] {
212
+ if (children == null || typeof children === "boolean") return [];
213
+ if (Array.isArray(children)) return children.flatMap(flattenQuantityPickerChildren);
214
+ if (
215
+ React.isValidElement<{ children?: React.ReactNode }>(children) &&
216
+ children.type === React.Fragment
217
+ ) {
218
+ return flattenQuantityPickerChildren(children.props.children);
219
+ }
220
+ return [children];
221
+ }
222
+
223
+ function withDividers(children: React.ReactNode, dividerClassName: string) {
224
+ const childArray = flattenQuantityPickerChildren(children);
225
+ return childArray.flatMap((child, index) => {
226
+ const nextChild = childArray[index + 1];
227
+ const shouldInsertDivider =
228
+ (isQuantityPickerButton(child) && isQuantityPickerValueDisplay(nextChild)) ||
229
+ (isQuantityPickerValueDisplay(child) && isQuantityPickerButton(nextChild));
230
+ if (!shouldInsertDivider) return [child];
231
+ return [
232
+ child,
233
+ <view
234
+ key={
235
+ isQuantityPickerValueDisplay(child)
236
+ ? "quantity-picker-divider-after-value"
237
+ : "quantity-picker-divider-before-value"
238
+ }
239
+ accessibility-elements-hidden={true}
240
+ className={dividerClassName}
241
+ />,
242
+ ];
243
+ });
244
+ }
245
+
246
+ function renderActionContent(
247
+ icon: React.ReactNode,
248
+ loadingIndicator: React.ReactNode,
249
+ children: React.ReactNode,
250
+ className: string,
251
+ loading: boolean,
252
+ ) {
253
+ if (loading) {
254
+ const content = loadingIndicator ?? children;
255
+ return (
256
+ <view className={className} accessibility-elements-hidden={true}>
257
+ {typeof content === "string" || typeof content === "number" ? (
258
+ <text>{content}</text>
259
+ ) : (
260
+ content
261
+ )}
262
+ </view>
263
+ );
264
+ }
265
+
266
+ if (icon == null) {
267
+ return typeof children === "string" || typeof children === "number" ? (
268
+ <text className={className}>{children}</text>
269
+ ) : (
270
+ children
271
+ );
272
+ }
273
+
274
+ if (typeof icon === "string" || typeof icon === "number") {
275
+ return <text className={className}>{icon}</text>;
276
+ }
277
+
278
+ if (!React.isValidElement<LynxIconElementProps>(icon)) return icon;
279
+ return <InternalIcon icon={icon} className={className} accessibility-elements-hidden={true} />;
280
+ }
281
+
282
+ export const QuantityPickerRoot = React.forwardRef<unknown, QuantityPickerRootProps>(
283
+ (props, ref) => {
284
+ const { loading, ...propsWithoutLoading } = props;
285
+ const [variantProps, otherProps] = quantityPicker.splitVariantProps(propsWithoutLoading);
286
+ const {
287
+ disabled = false,
288
+ invalid = false,
289
+ layout: layoutProp = "hug",
290
+ size: sizeProp = "medium",
291
+ } = variantProps;
292
+ const {
293
+ children,
294
+ className,
295
+ min,
296
+ max,
297
+ step: stepProp = 1,
298
+ value: valueProp,
299
+ defaultValue,
300
+ onValueChange,
301
+ readOnly = false,
302
+ onRemove,
303
+ getValueText = defaultGetValueText,
304
+ dir = "ltr",
305
+ removable = false,
306
+ removeAccessibilityLabel,
307
+ "accessibility-element": accessibilityElement = true,
308
+ "accessibility-role-description": accessibilityRoleDescription = "quantity picker",
309
+ "accessibility-traits": accessibilityTraits,
310
+ "accessibility-value": accessibilityValue,
311
+ ...nativeProps
312
+ } = otherProps;
313
+
314
+ validateProps({ min, max, step: stepProp, value: valueProp, defaultValue });
315
+ const initialValue = defaultValue ?? min;
316
+ assertSafeInteger(initialValue, "defaultValue");
317
+ if (initialValue < min || initialValue > max) {
318
+ throw new Error("QuantityPicker: defaultValue must be between min and max.");
319
+ }
320
+
321
+ const [value, setValue] = useControllableState({
322
+ value: valueProp,
323
+ defaultValue: initialValue,
324
+ onChange: onValueChange,
325
+ });
326
+ const loadingState = getLoadingState(loading);
327
+ const isAtMin = value === min;
328
+ const isAtMax = value === max;
329
+ const isRemoveButton = removable && isAtMin;
330
+ const decrementDisabled = disabled || (!isRemoveButton && isAtMin);
331
+ const incrementDisabled = disabled || isAtMax;
332
+ const decrementBlocked = decrementDisabled || readOnly || loadingState.decrement;
333
+ const incrementBlocked = incrementDisabled || readOnly || loadingState.increment;
334
+
335
+ const decrement = React.useCallback(() => {
336
+ "background only";
337
+ if (isRemoveButton) {
338
+ if (!decrementBlocked) onRemove?.();
339
+ return;
340
+ }
341
+ if (!decrementBlocked) setValue(Math.max(value - stepProp, min));
342
+ }, [decrementBlocked, isRemoveButton, min, onRemove, setValue, stepProp, value]);
343
+ const increment = React.useCallback(() => {
344
+ "background only";
345
+ if (!incrementBlocked) setValue(Math.min(value + stepProp, max));
346
+ }, [incrementBlocked, max, setValue, stepProp, value]);
347
+
348
+ const classes = quantityPicker(
349
+ recipeProps({
350
+ layout: layoutProp,
351
+ size: sizeProp,
352
+ invalid,
353
+ disabled,
354
+ loading: loadingState.decrement || loadingState.increment,
355
+ }),
356
+ );
357
+ const displayValue = getValueText(String(value), value);
358
+ const contextValue = React.useMemo<QuantityPickerContextValue>(
359
+ () => ({
360
+ layout: layoutProp,
361
+ size: sizeProp,
362
+ invalid,
363
+ disabled,
364
+ readOnly,
365
+ loading: loadingState.decrement || loadingState.increment,
366
+ value,
367
+ min,
368
+ max,
369
+ step: stepProp,
370
+ dir,
371
+ removable,
372
+ isAtMin,
373
+ isAtMax,
374
+ isRemoveButton,
375
+ removeAccessibilityLabel,
376
+ getValueText,
377
+ decrementLoading: loadingState.decrement,
378
+ incrementLoading: loadingState.increment,
379
+ decrementBlocked,
380
+ incrementBlocked,
381
+ decrement,
382
+ increment,
383
+ onRemove,
384
+ classes,
385
+ }),
386
+ [
387
+ classes,
388
+ decrement,
389
+ decrementBlocked,
390
+ disabled,
391
+ dir,
392
+ getValueText,
393
+ increment,
394
+ incrementBlocked,
395
+ invalid,
396
+ isAtMax,
397
+ isAtMin,
398
+ isRemoveButton,
399
+ loadingState.decrement,
400
+ loadingState.increment,
401
+ max,
402
+ min,
403
+ onRemove,
404
+ readOnly,
405
+ removeAccessibilityLabel,
406
+ removable,
407
+ layoutProp,
408
+ sizeProp,
409
+ value,
410
+ ],
411
+ );
412
+
413
+ const orderedChildren = flattenQuantityPickerChildren(children);
414
+ if (dir === "rtl") orderedChildren.reverse();
415
+ return (
416
+ <QuantityPickerContext.Provider value={contextValue}>
417
+ <view
418
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
419
+ className={clsx(classes.root, className)}
420
+ accessibility-element={accessibilityElement}
421
+ accessibility-role-description={accessibilityRoleDescription}
422
+ accessibility-traits={accessibilityTraits ?? (disabled ? "disabled" : undefined)}
423
+ accessibility-value={accessibilityValue ?? getAccessibleText(displayValue, value)}
424
+ >
425
+ {withDividers(orderedChildren, classes.divider)}
426
+ </view>
427
+ </QuantityPickerContext.Provider>
428
+ );
429
+ },
430
+ );
431
+ QuantityPickerRoot.displayName = "QuantityPickerRoot";
432
+
433
+ export const QuantityPickerDecrementButton = React.forwardRef<
434
+ unknown,
435
+ QuantityPickerDecrementButtonProps
436
+ >((props, ref) => {
437
+ const context = useQuantityPickerContext("QuantityPickerDecrementButton");
438
+ const {
439
+ children,
440
+ className,
441
+ style,
442
+ icon,
443
+ loadingIndicator,
444
+ removeIcon,
445
+ bindtap,
446
+ "main-thread:bindtap": mainThreadBindtap,
447
+ "accessibility-element": accessibilityElement = true,
448
+ "accessibility-label": accessibilityLabel,
449
+ "accessibility-role-description": accessibilityRoleDescription = "button",
450
+ "accessibility-traits": accessibilityTraits,
451
+ ...nativeProps
452
+ } = props;
453
+ const { pressed, bindtouchstart, bindtouchend, bindtouchcancel, ...pressHandlers } = usePressTap({
454
+ disabled: context.decrementBlocked,
455
+ onTap: (...args) => {
456
+ context.decrement();
457
+ bindtap?.(...args);
458
+ },
459
+ mainThreadOnTap: mainThreadBindtap,
460
+ });
461
+ const classes = quantityPicker(
462
+ recipeProps(
463
+ { ...context, loading: context.decrementLoading, disabled: context.decrementBlocked },
464
+ pressed && !context.decrementBlocked,
465
+ ),
466
+ );
467
+ const scaleFeedback = useScaleFeedback({
468
+ disabled: context.decrementBlocked,
469
+ onTouchStart: bindtouchstart,
470
+ onTouchEnd: bindtouchend,
471
+ onTouchCancel: bindtouchcancel,
472
+ });
473
+ const iconContent = context.isRemoveButton ? (removeIcon ?? icon) : icon;
474
+ return (
475
+ <view
476
+ {...mergeProps(
477
+ ref ? { ref: ref as LynxViewRef } : {},
478
+ scaleFeedback.scaleFeedbackTargetProps,
479
+ scaleFeedback.scaleFeedbackTriggerProps,
480
+ pressHandlers,
481
+ nativeProps,
482
+ )}
483
+ className={clsx(classes.decrementButton, className)}
484
+ style={style}
485
+ accessibility-element={accessibilityElement}
486
+ accessibility-label={
487
+ context.isRemoveButton ? context.removeAccessibilityLabel : accessibilityLabel
488
+ }
489
+ accessibility-role-description={accessibilityRoleDescription}
490
+ accessibility-traits={
491
+ accessibilityTraits ?? (context.decrementBlocked ? "disabled" : "button")
492
+ }
493
+ >
494
+ {renderActionContent(
495
+ iconContent,
496
+ loadingIndicator,
497
+ children,
498
+ classes.decrementIcon,
499
+ context.decrementLoading,
500
+ )}
501
+ </view>
502
+ );
503
+ });
504
+ QuantityPickerDecrementButton.displayName = "QuantityPickerDecrementButton";
505
+
506
+ export const QuantityPickerValueDisplay = React.forwardRef<
507
+ unknown,
508
+ QuantityPickerValueDisplayProps
509
+ >((props, ref) => {
510
+ const context = useQuantityPickerContext("QuantityPickerValueDisplay");
511
+ const {
512
+ className,
513
+ style,
514
+ "accessibility-elements-hidden": accessibilityElementsHidden = true,
515
+ ...nativeProps
516
+ } = props;
517
+ const valueText = context.getValueText(String(context.value), context.value);
518
+ const placeholderText = getValueDisplayPlaceholder(context.min, context.max);
519
+ return (
520
+ <view
521
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
522
+ className={clsx(quantityPicker(recipeProps(context)).valueDisplay, className)}
523
+ style={style}
524
+ accessibility-elements-hidden={accessibilityElementsHidden}
525
+ >
526
+ <text
527
+ accessibility-elements-hidden={true}
528
+ className={quantityPicker(recipeProps(context)).valueDisplayPlaceholder}
529
+ >
530
+ {context.getValueText(placeholderText, placeholderText)}
531
+ </text>
532
+ <text className={quantityPicker(recipeProps(context)).valueDisplayText}>{valueText}</text>
533
+ </view>
534
+ );
535
+ });
536
+ QuantityPickerValueDisplay.displayName = "QuantityPickerValueDisplay";
537
+
538
+ export const QuantityPickerIncrementButton = React.forwardRef<
539
+ unknown,
540
+ QuantityPickerIncrementButtonProps
541
+ >((props, ref) => {
542
+ const context = useQuantityPickerContext("QuantityPickerIncrementButton");
543
+ const {
544
+ children,
545
+ className,
546
+ style,
547
+ icon,
548
+ loadingIndicator,
549
+ bindtap,
550
+ "main-thread:bindtap": mainThreadBindtap,
551
+ "accessibility-element": accessibilityElement = true,
552
+ "accessibility-label": accessibilityLabel,
553
+ "accessibility-role-description": accessibilityRoleDescription = "button",
554
+ "accessibility-traits": accessibilityTraits,
555
+ ...nativeProps
556
+ } = props;
557
+ const { pressed, bindtouchstart, bindtouchend, bindtouchcancel, ...pressHandlers } = usePressTap({
558
+ disabled: context.incrementBlocked,
559
+ onTap: (...args) => {
560
+ context.increment();
561
+ bindtap?.(...args);
562
+ },
563
+ mainThreadOnTap: mainThreadBindtap,
564
+ });
565
+ const classes = quantityPicker(
566
+ recipeProps(
567
+ { ...context, loading: context.incrementLoading, disabled: context.incrementBlocked },
568
+ pressed && !context.incrementBlocked,
569
+ ),
570
+ );
571
+ const scaleFeedback = useScaleFeedback({
572
+ disabled: context.incrementBlocked,
573
+ onTouchStart: bindtouchstart,
574
+ onTouchEnd: bindtouchend,
575
+ onTouchCancel: bindtouchcancel,
576
+ });
577
+ return (
578
+ <view
579
+ {...mergeProps(
580
+ ref ? { ref: ref as LynxViewRef } : {},
581
+ scaleFeedback.scaleFeedbackTargetProps,
582
+ scaleFeedback.scaleFeedbackTriggerProps,
583
+ pressHandlers,
584
+ nativeProps,
585
+ )}
586
+ className={clsx(classes.incrementButton, className)}
587
+ style={style}
588
+ accessibility-element={accessibilityElement}
589
+ accessibility-label={accessibilityLabel}
590
+ accessibility-role-description={accessibilityRoleDescription}
591
+ accessibility-traits={
592
+ accessibilityTraits ?? (context.incrementBlocked ? "disabled" : "button")
593
+ }
594
+ >
595
+ {renderActionContent(
596
+ icon,
597
+ loadingIndicator,
598
+ children,
599
+ classes.incrementIcon,
600
+ context.incrementLoading,
601
+ )}
602
+ </view>
603
+ );
604
+ });
605
+ QuantityPickerIncrementButton.displayName = "QuantityPickerIncrementButton";
@@ -0,0 +1,14 @@
1
+ export {
2
+ QuantityPickerRoot,
3
+ QuantityPickerDecrementButton,
4
+ QuantityPickerValueDisplay,
5
+ QuantityPickerIncrementButton,
6
+ type QuantityPickerRootProps,
7
+ type QuantityPickerDecrementButtonProps,
8
+ type QuantityPickerValueDisplayProps,
9
+ type QuantityPickerIncrementButtonProps,
10
+ type QuantityPickerLoading,
11
+ type QuantityPickerGetValueText,
12
+ } from "./QuantityPicker";
13
+
14
+ export * as QuantityPicker from "./QuantityPicker.namespace";
@@ -0,0 +1,50 @@
1
+ import { runOnBackground, useMainThreadRef } from "@lynx-js/react";
2
+ import { fireEvent, render, waitSchedule } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { ReactionButton } from "./ReactionButton";
5
+
6
+ describe("ReactionButton feedback integration", () => {
7
+ it("preserves the user's Main Thread ref and touch handler and clears press on cancel", async () => {
8
+ function Example({ report }: { report: (attached: boolean) => void }) {
9
+ const userRef = useMainThreadRef(null);
10
+ function handleTouch() {
11
+ "main thread";
12
+ runOnBackground(report)(userRef.current !== null);
13
+ }
14
+ return (
15
+ <ReactionButton main-thread:ref={userRef} main-thread:bindtouchstart={handleTouch}>
16
+ 좋아요
17
+ </ReactionButton>
18
+ );
19
+ }
20
+ const report = vi.fn();
21
+ const { container } = render(<Example report={report} />, {
22
+ enableMainThread: true,
23
+ enableBackgroundThread: true,
24
+ });
25
+ await waitSchedule();
26
+ const target = container.querySelector(".seed-reaction-button__root")!;
27
+ fireEvent.touchstart(target, {});
28
+ await waitSchedule();
29
+ expect(report.mock.calls).toEqual([[true]]);
30
+ expect(target.classList.contains("seed-reaction-button__root--pressed_true")).toBe(true);
31
+ fireEvent.touchcancel(target, {});
32
+ await waitSchedule();
33
+ expect(target.classList.contains("seed-reaction-button__root--pressed_true")).toBe(false);
34
+ });
35
+
36
+ it.each([
37
+ "disabled",
38
+ "loading",
39
+ ] as const)("does not enter pressed state while %s", async (state) => {
40
+ const { container } = render(<ReactionButton {...{ [state]: true }}>좋아요</ReactionButton>, {
41
+ enableMainThread: true,
42
+ enableBackgroundThread: true,
43
+ });
44
+ await waitSchedule();
45
+ const target = container.querySelector(".seed-reaction-button__root")!;
46
+ fireEvent.touchstart(target, {});
47
+ await waitSchedule();
48
+ expect(target.classList.contains("seed-reaction-button__root--pressed_true")).toBe(false);
49
+ });
50
+ });
@@ -69,7 +69,7 @@ describe("ReactionButton", () => {
69
69
  const queries = getQueriesForElement(getRenderedRoot());
70
70
 
71
71
  expect(root).toHaveClass("seed-reaction-button__root--size_small");
72
- expect(root.querySelector(".seed-prefix-icon")).toHaveClass("seed-reaction-button__prefixIcon");
72
+ expect(root.querySelector(".seed-prefix-icon-slot")).toHaveClass("seed-reaction-button__prefixIcon");
73
73
  expect(queries.getByText("좋아요")).toHaveClass("seed-reaction-button__label");
74
74
  expect(queries.getByText("12")).toHaveClass("seed-count", "seed-reaction-button__count");
75
75
 
@@ -96,10 +96,11 @@ describe("ReactionButton", () => {
96
96
  expect(root).toHaveAttribute("accessibility-traits", "disabled");
97
97
  });
98
98
 
99
- it("keeps content mounted and handles taps while loading", () => {
99
+ it("keeps content mounted but ignores taps while loading", () => {
100
100
  const onPressedChange = vi.fn();
101
+ const onTap = vi.fn();
101
102
  render(
102
- <ReactionButton loading onPressedChange={onPressedChange}>
103
+ <ReactionButton loading onPressedChange={onPressedChange} bindtap={onTap}>
103
104
  좋아요
104
105
  <Count>12</Count>
105
106
  </ReactionButton>,
@@ -117,6 +118,7 @@ describe("ReactionButton", () => {
117
118
 
118
119
  fireEvent.tap(root);
119
120
 
120
- expect(onPressedChange).toHaveBeenCalledWith(true);
121
+ expect(onPressedChange).not.toHaveBeenCalled();
122
+ expect(onTap).not.toHaveBeenCalled();
121
123
  });
122
124
  });