@seed-design/lynx-react 0.6.3 → 0.7.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 (234) hide show
  1. package/lib/components/Accordion/Accordion.d.ts.map +1 -1
  2. package/lib/components/Accordion/Accordion.jsx +24 -14
  3. package/lib/components/ActionButton/ActionButton.d.ts +1 -1
  4. package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
  5. package/lib/components/ActionButton/ActionButton.jsx +12 -4
  6. package/lib/components/AppBar/AppBar.d.ts.map +1 -1
  7. package/lib/components/AppBar/AppBar.jsx +9 -8
  8. package/lib/components/Badge/Badge.d.ts.map +1 -1
  9. package/lib/components/Badge/Badge.jsx +2 -1
  10. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  11. package/lib/components/BottomSheet/BottomSheet.jsx +9 -9
  12. package/lib/components/Box/Box.d.ts.map +1 -1
  13. package/lib/components/Box/Box.jsx +2 -1
  14. package/lib/components/Callout/Callout.d.ts +2 -2
  15. package/lib/components/Callout/Callout.d.ts.map +1 -1
  16. package/lib/components/Callout/Callout.jsx +20 -10
  17. package/lib/components/Checkbox/Checkbox.d.ts +4 -2
  18. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
  19. package/lib/components/Checkbox/Checkbox.jsx +33 -17
  20. package/lib/components/Chip/Chip.d.ts +1 -1
  21. package/lib/components/Chip/Chip.d.ts.map +1 -1
  22. package/lib/components/Chip/Chip.jsx +36 -16
  23. package/lib/components/Count/Count.d.ts +7 -0
  24. package/lib/components/Count/Count.d.ts.map +1 -0
  25. package/lib/components/Count/Count.jsx +19 -0
  26. package/lib/components/Count/index.d.ts +2 -0
  27. package/lib/components/Count/index.d.ts.map +1 -0
  28. package/lib/components/Divider/Divider.d.ts.map +1 -1
  29. package/lib/components/Divider/Divider.jsx +2 -1
  30. package/lib/components/Field/Field.d.ts.map +1 -1
  31. package/lib/components/Field/Field.jsx +11 -15
  32. package/lib/components/Icon/Icon.d.ts.map +1 -1
  33. package/lib/components/Icon/Icon.jsx +4 -3
  34. package/lib/components/InputButton/InputButton.d.ts +40 -0
  35. package/lib/components/InputButton/InputButton.d.ts.map +1 -0
  36. package/lib/components/InputButton/InputButton.jsx +118 -0
  37. package/lib/components/InputButton/InputButton.namespace.d.ts +2 -0
  38. package/lib/components/InputButton/InputButton.namespace.d.ts.map +1 -0
  39. package/lib/components/InputButton/InputButton.namespace.js +16 -0
  40. package/lib/components/InputButton/index.d.ts +3 -0
  41. package/lib/components/InputButton/index.d.ts.map +1 -0
  42. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts.map +1 -1
  43. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.jsx +12 -8
  44. package/lib/components/List/List.d.ts +51 -0
  45. package/lib/components/List/List.d.ts.map +1 -0
  46. package/lib/components/List/List.jsx +176 -0
  47. package/lib/components/List/List.namespace.d.ts +2 -0
  48. package/lib/components/List/List.namespace.d.ts.map +1 -0
  49. package/lib/components/List/List.namespace.js +18 -0
  50. package/lib/components/List/index.d.ts +3 -0
  51. package/lib/components/List/index.d.ts.map +1 -0
  52. package/lib/components/MannerTemp/MannerTemp.d.ts.map +1 -1
  53. package/lib/components/MannerTemp/MannerTemp.jsx +3 -2
  54. package/lib/components/MannerTempBadge/MannerTempBadge.d.ts.map +1 -1
  55. package/lib/components/MannerTempBadge/MannerTempBadge.jsx +2 -1
  56. package/lib/components/Menu/Menu.d.ts +69 -0
  57. package/lib/components/Menu/Menu.d.ts.map +1 -0
  58. package/lib/components/Menu/Menu.jsx +520 -0
  59. package/lib/components/Menu/Menu.namespace.d.ts +2 -0
  60. package/lib/components/Menu/Menu.namespace.d.ts.map +1 -0
  61. package/lib/components/Menu/Menu.namespace.js +21 -0
  62. package/lib/components/Menu/index.d.ts +3 -0
  63. package/lib/components/Menu/index.d.ts.map +1 -0
  64. package/lib/components/NotificationBadge/NotificationBadge.d.ts.map +1 -1
  65. package/lib/components/NotificationBadge/NotificationBadge.jsx +3 -2
  66. package/lib/components/PageBanner/PageBanner.d.ts +5 -5
  67. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
  68. package/lib/components/PageBanner/PageBanner.jsx +25 -14
  69. package/lib/components/ProgressCircle/ProgressCircle.d.ts +2 -2
  70. package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
  71. package/lib/components/ProgressCircle/ProgressCircle.jsx +4 -3
  72. package/lib/components/RadioGroup/RadioGroup.d.ts +5 -3
  73. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  74. package/lib/components/RadioGroup/RadioGroup.jsx +22 -9
  75. package/lib/components/ReactionButton/ReactionButton.d.ts +20 -0
  76. package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -0
  77. package/lib/components/ReactionButton/ReactionButton.jsx +88 -0
  78. package/lib/components/ReactionButton/index.d.ts +2 -0
  79. package/lib/components/ReactionButton/index.d.ts.map +1 -0
  80. package/lib/components/ScaleFeedback/ScaleFeedback.d.ts +11 -0
  81. package/lib/components/ScaleFeedback/ScaleFeedback.d.ts.map +1 -0
  82. package/lib/components/ScaleFeedback/ScaleFeedback.jsx +16 -0
  83. package/lib/components/ScaleFeedback/index.d.ts +2 -0
  84. package/lib/components/ScaleFeedback/index.d.ts.map +1 -0
  85. package/lib/components/SegmentedControl/SegmentedControl.d.ts +1 -0
  86. package/lib/components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  87. package/lib/components/SegmentedControl/SegmentedControl.jsx +23 -10
  88. package/lib/components/SelectBox/SelectBox.d.ts.map +1 -1
  89. package/lib/components/SelectBox/SelectBox.jsx +23 -13
  90. package/lib/components/Stack/Stack.d.ts.map +1 -1
  91. package/lib/components/Stack/Stack.jsx +2 -1
  92. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts +85 -0
  93. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -0
  94. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +256 -0
  95. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.d.ts +2 -0
  96. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.d.ts.map +1 -0
  97. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.js +24 -0
  98. package/lib/components/SwipeableMenuSheet/index.d.ts +3 -0
  99. package/lib/components/SwipeableMenuSheet/index.d.ts.map +1 -0
  100. package/lib/components/Switch/Switch.d.ts +27 -3
  101. package/lib/components/Switch/Switch.d.ts.map +1 -1
  102. package/lib/components/Switch/Switch.jsx +22 -9
  103. package/lib/components/Tabs/Tabs.d.ts +4 -2
  104. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  105. package/lib/components/Tabs/Tabs.jsx +37 -22
  106. package/lib/components/TagGroup/TagGroup.d.ts.map +1 -1
  107. package/lib/components/TagGroup/TagGroup.jsx +4 -3
  108. package/lib/components/Text/Text.d.ts.map +1 -1
  109. package/lib/components/Text/Text.jsx +2 -1
  110. package/lib/components/TextField/TextField.d.ts.map +1 -1
  111. package/lib/components/TextField/TextField.jsx +35 -23
  112. package/lib/components/index.d.ts +7 -0
  113. package/lib/components/index.d.ts.map +1 -1
  114. package/lib/components/private/Positioning.d.ts +69 -0
  115. package/lib/components/private/Positioning.d.ts.map +1 -0
  116. package/lib/components/private/Positioning.js +109 -0
  117. package/lib/components/private/TrackPositioning.d.ts +22 -0
  118. package/lib/components/private/TrackPositioning.d.ts.map +1 -0
  119. package/lib/contexts/ScaleFeedbackContentContext.d.ts +4 -0
  120. package/lib/contexts/ScaleFeedbackContentContext.d.ts.map +1 -0
  121. package/lib/contexts/ScaleFeedbackContentContext.js +6 -0
  122. package/lib/contexts/index.d.ts +2 -0
  123. package/lib/contexts/index.d.ts.map +1 -0
  124. package/lib/hooks/index.d.ts +1 -0
  125. package/lib/hooks/index.d.ts.map +1 -1
  126. package/lib/hooks/useIconColor.d.ts.map +1 -1
  127. package/lib/hooks/useIconColor.js +1 -1
  128. package/lib/hooks/usePressTap.d.ts +4 -3
  129. package/lib/hooks/usePressTap.d.ts.map +1 -1
  130. package/lib/hooks/useScaleFeedback.d.ts +48 -0
  131. package/lib/hooks/useScaleFeedback.d.ts.map +1 -0
  132. package/lib/hooks/useScaleFeedback.js +108 -0
  133. package/lib/index.d.ts +1 -0
  134. package/lib/index.d.ts.map +1 -1
  135. package/lib/index.js +17 -3
  136. package/lib/node_modules/.bun/@floating-ui_core@1.8.0/node_modules/@floating-ui/core/dist/floating-ui.core.js +493 -0
  137. package/lib/node_modules/.bun/@floating-ui_utils@0.2.12/node_modules/@floating-ui/utils/dist/floating-ui.utils.js +114 -0
  138. package/lib/utils/calculate-scale-feedback.d.ts +5 -0
  139. package/lib/utils/calculate-scale-feedback.d.ts.map +1 -0
  140. package/lib/utils/calculate-scale-feedback.js +13 -0
  141. package/lib/utils/compose-event-handlers.d.ts +5 -0
  142. package/lib/utils/compose-event-handlers.d.ts.map +1 -0
  143. package/lib/utils/compose-event-handlers.js +16 -0
  144. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -1
  145. package/lib/utils/create-slot-recipe-context.jsx +6 -16
  146. package/lib/utils/index.d.ts +1 -0
  147. package/lib/utils/index.d.ts.map +1 -1
  148. package/lib/utils/merge-props.d.ts +15 -0
  149. package/lib/utils/merge-props.d.ts.map +1 -0
  150. package/lib/utils/merge-props.js +36 -0
  151. package/lib/utils/merge-refs.d.ts +8 -0
  152. package/lib/utils/merge-refs.d.ts.map +1 -0
  153. package/lib/utils/merge-refs.js +38 -0
  154. package/package.json +6 -5
  155. package/src/components/Accordion/Accordion.tsx +36 -27
  156. package/src/components/ActionButton/ActionButton.tsx +27 -12
  157. package/src/components/AppBar/AppBar.tsx +17 -18
  158. package/src/components/Badge/Badge.tsx +2 -2
  159. package/src/components/BottomSheet/BottomSheet.tsx +9 -17
  160. package/src/components/Box/Box.tsx +2 -2
  161. package/src/components/Callout/Callout.tsx +39 -22
  162. package/src/components/Checkbox/Checkbox.test.tsx +44 -0
  163. package/src/components/Checkbox/Checkbox.tsx +52 -21
  164. package/src/components/Chip/Chip.test.tsx +50 -1
  165. package/src/components/Chip/Chip.tsx +75 -31
  166. package/src/components/Count/Count.tsx +35 -0
  167. package/src/components/Count/index.ts +1 -0
  168. package/src/components/Divider/Divider.tsx +2 -2
  169. package/src/components/Field/Field.tsx +15 -28
  170. package/src/components/Icon/Icon.tsx +14 -7
  171. package/src/components/InputButton/InputButton.namespace.ts +20 -0
  172. package/src/components/InputButton/InputButton.test.tsx +82 -0
  173. package/src/components/InputButton/InputButton.tsx +295 -0
  174. package/src/components/InputButton/index.ts +2 -0
  175. package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.tsx +16 -21
  176. package/src/components/List/List.namespace.ts +24 -0
  177. package/src/components/List/List.test.tsx +162 -0
  178. package/src/components/List/List.tsx +476 -0
  179. package/src/components/List/index.ts +3 -0
  180. package/src/components/MannerTemp/MannerTemp.tsx +3 -4
  181. package/src/components/MannerTempBadge/MannerTempBadge.tsx +2 -2
  182. package/src/components/Menu/Menu.namespace.ts +26 -0
  183. package/src/components/Menu/Menu.tsx +883 -0
  184. package/src/components/Menu/index.ts +31 -0
  185. package/src/components/NotificationBadge/NotificationBadge.tsx +4 -9
  186. package/src/components/PageBanner/PageBanner.test.tsx +3 -0
  187. package/src/components/PageBanner/PageBanner.tsx +54 -45
  188. package/src/components/ProgressCircle/ProgressCircle.test.tsx +48 -0
  189. package/src/components/ProgressCircle/ProgressCircle.tsx +22 -3
  190. package/src/components/RadioGroup/RadioGroup.test.tsx +87 -0
  191. package/src/components/RadioGroup/RadioGroup.tsx +50 -14
  192. package/src/components/ReactionButton/ReactionButton.test.tsx +122 -0
  193. package/src/components/ReactionButton/ReactionButton.tsx +139 -0
  194. package/src/components/ReactionButton/index.ts +1 -0
  195. package/src/components/ScaleFeedback/ScaleFeedback.test.tsx +41 -0
  196. package/src/components/ScaleFeedback/ScaleFeedback.tsx +32 -0
  197. package/src/components/ScaleFeedback/index.ts +1 -0
  198. package/src/components/SegmentedControl/SegmentedControl.test.tsx +15 -3
  199. package/src/components/SegmentedControl/SegmentedControl.tsx +35 -14
  200. package/src/components/SelectBox/SelectBox.test.tsx +39 -1
  201. package/src/components/SelectBox/SelectBox.tsx +27 -16
  202. package/src/components/Stack/Stack.tsx +2 -2
  203. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.ts +37 -0
  204. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +258 -0
  205. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +655 -0
  206. package/src/components/SwipeableMenuSheet/index.ts +43 -0
  207. package/src/components/Switch/Switch.test.tsx +57 -0
  208. package/src/components/Switch/Switch.tsx +60 -16
  209. package/src/components/Tabs/Tabs.test.tsx +19 -0
  210. package/src/components/Tabs/Tabs.tsx +72 -40
  211. package/src/components/TagGroup/TagGroup.tsx +4 -6
  212. package/src/components/Text/Text.tsx +2 -2
  213. package/src/components/TextField/TextField.tsx +60 -54
  214. package/src/components/index.ts +7 -0
  215. package/src/components/private/Positioning.test.ts +235 -0
  216. package/src/components/private/Positioning.ts +223 -0
  217. package/src/components/private/TrackPositioning.test.ts +58 -0
  218. package/src/components/private/TrackPositioning.ts +37 -0
  219. package/src/contexts/ScaleFeedbackContentContext.ts +4 -0
  220. package/src/contexts/index.ts +1 -0
  221. package/src/hooks/index.ts +1 -0
  222. package/src/hooks/useIconColor.ts +5 -4
  223. package/src/hooks/usePressTap.ts +4 -3
  224. package/src/hooks/useScaleFeedback.integration.test.tsx +85 -0
  225. package/src/hooks/useScaleFeedback.test.ts +50 -0
  226. package/src/hooks/useScaleFeedback.ts +199 -0
  227. package/src/index.ts +1 -0
  228. package/src/utils/calculate-scale-feedback.ts +12 -0
  229. package/src/utils/compose-event-handlers.ts +16 -0
  230. package/src/utils/create-slot-recipe-context.tsx +6 -7
  231. package/src/utils/index.ts +1 -0
  232. package/src/utils/merge-props.test.tsx +152 -0
  233. package/src/utils/merge-props.ts +62 -0
  234. package/src/utils/merge-refs.ts +44 -0
@@ -0,0 +1,122 @@
1
+ import "@testing-library/jest-dom";
2
+ import { fireEvent, getQueriesForElement, render } from "@lynx-js/react/testing-library";
3
+ import type { MainThread } from "@lynx-js/types";
4
+ import type * as LynxReact from "@lynx-js/react";
5
+ import * as React from "@lynx-js/react";
6
+ import { describe, expect, it, vi } from "vitest";
7
+
8
+ import type { LynxIconElementProps } from "../../types";
9
+ import { Count } from "../Count";
10
+ import { PrefixIcon } from "../Icon";
11
+ import { ReactionButton } from "./ReactionButton";
12
+
13
+ vi.mock("@lynx-js/react", async (importOriginal) => {
14
+ const actual = await importOriginal<typeof LynxReact>();
15
+
16
+ return {
17
+ ...actual,
18
+ runOnMainThread: () => () => undefined,
19
+ };
20
+ });
21
+
22
+ function getRenderedRoot() {
23
+ const root = elementTree.root;
24
+
25
+ if (!root) {
26
+ throw new Error("Expected Lynx render root to exist.");
27
+ }
28
+
29
+ return root;
30
+ }
31
+
32
+ function getReactionButtonRoot() {
33
+ const root = getRenderedRoot();
34
+
35
+ if (root.classList.contains("seed-reaction-button__root")) return root;
36
+
37
+ const reactionButtonRoot = root.querySelector<HTMLElement>(".seed-reaction-button__root");
38
+ if (!reactionButtonRoot) throw new Error("Expected ReactionButton root to exist.");
39
+
40
+ return reactionButtonRoot;
41
+ }
42
+
43
+ const MockIcon = React.forwardRef<MainThread.Element, LynxIconElementProps>((props, ref) => {
44
+ const { className, style, ...rest } = props;
45
+
46
+ return (
47
+ <image
48
+ {...rest}
49
+ {...(ref ? { "main-thread:ref": ref as React.Ref<MainThread.Element> } : {})}
50
+ className={className}
51
+ style={style}
52
+ />
53
+ );
54
+ });
55
+ MockIcon.displayName = "MockIcon";
56
+
57
+ describe("ReactionButton", () => {
58
+ it("composes slots and toggles uncontrolled selected state", () => {
59
+ const onPressedChange = vi.fn();
60
+ render(
61
+ <ReactionButton onPressedChange={onPressedChange}>
62
+ <PrefixIcon icon={<MockIcon />} />
63
+ 좋아요
64
+ <Count>12</Count>
65
+ </ReactionButton>,
66
+ );
67
+
68
+ const root = getReactionButtonRoot();
69
+ const queries = getQueriesForElement(getRenderedRoot());
70
+
71
+ expect(root).toHaveClass("seed-reaction-button__root--size_small");
72
+ expect(root.querySelector(".seed-prefix-icon")).toHaveClass("seed-reaction-button__prefixIcon");
73
+ expect(queries.getByText("좋아요")).toHaveClass("seed-reaction-button__label");
74
+ expect(queries.getByText("12")).toHaveClass("seed-count", "seed-reaction-button__count");
75
+
76
+ fireEvent.tap(root);
77
+
78
+ expect(onPressedChange).toHaveBeenCalledWith(true);
79
+ expect(getReactionButtonRoot()).toHaveClass("seed-reaction-button__root--selected_true");
80
+ expect(getReactionButtonRoot()).toHaveAttribute("accessibility-traits", "selected");
81
+ });
82
+
83
+ it("does not toggle when disabled", () => {
84
+ const onPressedChange = vi.fn();
85
+ render(
86
+ <ReactionButton disabled onPressedChange={onPressedChange}>
87
+ 좋아요
88
+ </ReactionButton>,
89
+ );
90
+
91
+ const root = getReactionButtonRoot();
92
+ fireEvent.tap(root);
93
+
94
+ expect(onPressedChange).not.toHaveBeenCalled();
95
+ expect(root).toHaveClass("seed-reaction-button__root--disabled_true");
96
+ expect(root).toHaveAttribute("accessibility-traits", "disabled");
97
+ });
98
+
99
+ it("keeps content mounted and handles taps while loading", () => {
100
+ const onPressedChange = vi.fn();
101
+ render(
102
+ <ReactionButton loading onPressedChange={onPressedChange}>
103
+ 좋아요
104
+ <Count>12</Count>
105
+ </ReactionButton>,
106
+ );
107
+
108
+ const root = getReactionButtonRoot();
109
+ const queries = getQueriesForElement(getRenderedRoot());
110
+ const content = root.querySelector(".seed-reaction-button__content");
111
+
112
+ expect(content).toHaveClass("seed-reaction-button__content--loading_true");
113
+ expect(queries.getByText("좋아요")).toBeInTheDocument();
114
+ expect(queries.getByText("12")).toBeInTheDocument();
115
+ expect(root.querySelector(".seed-reaction-button__loadingIndicator")).toBeInTheDocument();
116
+ expect(root.querySelector(".seed-progress-circle__root")).toBeInTheDocument();
117
+
118
+ fireEvent.tap(root);
119
+
120
+ expect(onPressedChange).toHaveBeenCalledWith(true);
121
+ });
122
+ });
@@ -0,0 +1,139 @@
1
+ import { reactionButton } from "@seed-design/lynx-css/recipes/reaction-button";
2
+ import type { ReactionButtonVariantProps } from "@seed-design/lynx-css/recipes/reaction-button";
3
+ import clsx from "clsx";
4
+ import * as React from "@lynx-js/react";
5
+ import { cloneElement, useMemo } from "@lynx-js/react";
6
+
7
+ import { useControllableState } from "../../hooks/useControllableState";
8
+ import { usePressTap } from "../../hooks/usePressTap";
9
+ import type {
10
+ LynxAccessibilityProps,
11
+ LynxPressableProps,
12
+ LynxStyledElementProps,
13
+ LynxViewRef,
14
+ } from "../../types";
15
+ import { toArray } from "../../utils/children";
16
+ import { isCountElement, type CountProps } from "../Count/Count";
17
+ import { getIconSlotName, IconSlotProvider } from "../Icon/Icon";
18
+ import { ProgressCircleRange, ProgressCircleRoot } from "../ProgressCircle";
19
+
20
+ /**
21
+ * @platform Lynx
22
+ *
23
+ * 웹 대비 미지원 기능:
24
+ * - HTML button 속성 및 native form 제출
25
+ * - 키보드 focus / focusVisible
26
+ * - `asChild`
27
+ */
28
+ export interface ReactionButtonProps
29
+ extends Omit<ReactionButtonVariantProps, "selected" | "pressed" | "disabled" | "loading">,
30
+ LynxStyledElementProps,
31
+ LynxPressableProps,
32
+ LynxAccessibilityProps {
33
+ pressed?: boolean;
34
+ defaultPressed?: boolean;
35
+ onPressedChange?: (pressed: boolean) => void;
36
+ disabled?: boolean;
37
+ loading?: boolean;
38
+ }
39
+
40
+ export const ReactionButton = React.forwardRef<unknown, ReactionButtonProps>((props, ref) => {
41
+ const [variantProps, otherProps] = reactionButton.splitVariantProps(props);
42
+ const {
43
+ children,
44
+ className,
45
+ style,
46
+ defaultPressed = false,
47
+ onPressedChange,
48
+ bindtap,
49
+ "main-thread:bindtap": mainThreadBindtap,
50
+ "accessibility-element": accessibilityElement = true,
51
+ "accessibility-traits": accessibilityTraits,
52
+ ...nativeProps
53
+ } = otherProps;
54
+ const {
55
+ size,
56
+ pressed: pressedProp,
57
+ disabled = false,
58
+ loading = false,
59
+ } = variantProps;
60
+ const [selected, setSelected] = useControllableState({
61
+ value: pressedProp,
62
+ defaultValue: defaultPressed,
63
+ onChange: onPressedChange,
64
+ });
65
+ const handleTap = React.useCallback(
66
+ (...args: Parameters<NonNullable<LynxPressableProps["bindtap"]>>) => {
67
+ setSelected(!selected);
68
+ bindtap?.(...args);
69
+ },
70
+ [bindtap, selected, setSelected],
71
+ );
72
+ const { pressed, ...pressHandlers } = usePressTap({
73
+ disabled,
74
+ onTap: handleTap,
75
+ mainThreadOnTap: mainThreadBindtap,
76
+ });
77
+ const classNames = reactionButton({ ...variantProps, selected, pressed, disabled, loading });
78
+ const iconSlotContextValue = useMemo(
79
+ () => ({
80
+ classNames: { prefixIcon: classNames.prefixIcon },
81
+ deps: [size ?? "small", selected, pressed, disabled, loading],
82
+ }),
83
+ [classNames.prefixIcon, size, selected, pressed, disabled, loading],
84
+ );
85
+
86
+ const prefixIconChildren: React.ReactNode[] = [];
87
+ const labelChildren: React.ReactNode[] = [];
88
+ const countChildren: React.ReactElement<CountProps>[] = [];
89
+
90
+ for (const child of toArray(children)) {
91
+ if (getIconSlotName(child) === "prefixIcon") {
92
+ prefixIconChildren.push(child);
93
+ continue;
94
+ }
95
+
96
+ if (isCountElement(child)) {
97
+ countChildren.push(child);
98
+ continue;
99
+ }
100
+
101
+ labelChildren.push(child);
102
+ }
103
+
104
+ return (
105
+ <IconSlotProvider value={iconSlotContextValue}>
106
+ <view
107
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
108
+ {...nativeProps}
109
+ className={clsx(classNames.root, className)}
110
+ style={style}
111
+ accessibility-element={accessibilityElement}
112
+ accessibility-traits={
113
+ disabled ? "disabled" : selected ? "selected" : (accessibilityTraits ?? "button")
114
+ }
115
+ {...pressHandlers}
116
+ >
117
+ <view className={classNames.content}>
118
+ {prefixIconChildren}
119
+ {labelChildren.length > 0 ? (
120
+ <text className={classNames.label}>{labelChildren}</text>
121
+ ) : null}
122
+ {countChildren.map((child) =>
123
+ cloneElement(child, {
124
+ className: clsx(classNames.count, child.props.className),
125
+ }),
126
+ )}
127
+ </view>
128
+ {loading ? (
129
+ <view className={classNames.loadingIndicator}>
130
+ <ProgressCircleRoot size="14" tone="inherit">
131
+ <ProgressCircleRange />
132
+ </ProgressCircleRoot>
133
+ </view>
134
+ ) : null}
135
+ </view>
136
+ </IconSlotProvider>
137
+ );
138
+ });
139
+ ReactionButton.displayName = "ReactionButton";
@@ -0,0 +1 @@
1
+ export { ReactionButton, type ReactionButtonProps } from "./ReactionButton";
@@ -0,0 +1,41 @@
1
+ import { createRef } from "@lynx-js/react";
2
+ import { render, waitSchedule } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { ScaleFeedback } from "./ScaleFeedback";
6
+
7
+ describe("ScaleFeedback", () => {
8
+ it("forwards the consumer ref and clears it on unmount", async () => {
9
+ const ref = createRef<unknown>();
10
+ const { unmount } = render(
11
+ <ScaleFeedback ref={ref}>
12
+ <view />
13
+ </ScaleFeedback>,
14
+ { enableMainThread: true, enableBackgroundThread: true },
15
+ );
16
+ await waitSchedule();
17
+
18
+ expect(ref.current).toBeTruthy();
19
+ unmount();
20
+ await waitSchedule();
21
+ expect(ref.current).toBeNull();
22
+ });
23
+
24
+ it("renders its child inside one native feedback target", async () => {
25
+ const { container } = render(
26
+ <ScaleFeedback>
27
+ <view id="target" />
28
+ </ScaleFeedback>,
29
+ { enableMainThread: true, enableBackgroundThread: false },
30
+ );
31
+ await waitSchedule();
32
+
33
+ const target = container.querySelector("#target");
34
+ const feedbackTarget = container.children[0];
35
+
36
+ expect(target).not.toBeNull();
37
+ expect(container.children).toHaveLength(1);
38
+ expect(feedbackTarget).not.toBe(target);
39
+ expect(feedbackTarget.children).toHaveLength(1);
40
+ });
41
+ });
@@ -0,0 +1,32 @@
1
+ import * as React from "@lynx-js/react";
2
+ import { useScaleFeedback, type UseScaleFeedbackOptions } from "../../hooks/useScaleFeedback";
3
+ import type { LynxViewRef } from "../../types";
4
+ import { mergeProps } from "../../utils/merge-props";
5
+
6
+ export interface ScaleFeedbackProps extends UseScaleFeedbackOptions {
7
+ /** Content rendered inside the native element that receives Self Scale Feedback. */
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ /**
12
+ * Applies Self Scale Feedback through a native Lynx view.
13
+ */
14
+ export const ScaleFeedback = React.forwardRef<unknown, ScaleFeedbackProps>(
15
+ ({ children, ...options }, ref) => {
16
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback(options);
17
+
18
+ return (
19
+ <view
20
+ {...mergeProps(
21
+ ref ? { ref: ref as LynxViewRef } : {},
22
+ scaleFeedbackTriggerProps,
23
+ scaleFeedbackTargetProps,
24
+ )}
25
+ >
26
+ {children}
27
+ </view>
28
+ );
29
+ },
30
+ );
31
+
32
+ ScaleFeedback.displayName = "ScaleFeedback";
@@ -0,0 +1 @@
1
+ export * from "./ScaleFeedback";
@@ -1,5 +1,5 @@
1
1
  import "@testing-library/jest-dom";
2
- import { fireEvent, render } from "@lynx-js/react/testing-library";
2
+ import { fireEvent, render, waitSchedule } from "@lynx-js/react/testing-library";
3
3
  import { describe, expect, it, vi } from "vitest";
4
4
 
5
5
  import * as SegmentedControl from "./SegmentedControl.namespace";
@@ -62,19 +62,31 @@ describe("SegmentedControl", () => {
62
62
  expect(disabledItem).toHaveAttribute("accessibility-traits", "disabled");
63
63
  });
64
64
 
65
- it("keeps the press-start selection on the fading item background", () => {
66
- const { container } = render(<BasicSegmentedControl defaultValue="hot" />);
65
+ it("keeps the press-start selection when main-thread feedback hands off to selection", async () => {
66
+ const onValueChange = vi.fn();
67
+ const { container } = render(
68
+ <BasicSegmentedControl defaultValue="hot" onValueChange={onValueChange} />,
69
+ {
70
+ enableMainThread: true,
71
+ enableBackgroundThread: true,
72
+ },
73
+ );
74
+ await waitSchedule();
67
75
  const latest = getItem(container, "최신순");
68
76
  const getBackground = () =>
69
77
  latest.querySelector<HTMLElement>(".seed-segmented-control__itemBackground");
70
78
 
71
79
  fireEvent.touchstart(latest, {});
80
+ await waitSchedule();
72
81
  expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--pressed_true");
73
82
  expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--selected_false");
74
83
 
75
84
  fireEvent.touchend(latest, {});
85
+ await waitSchedule();
76
86
  fireEvent.tap(latest);
87
+ await waitSchedule();
77
88
 
89
+ expect(onValueChange).toHaveBeenCalledExactlyOnceWith("new");
78
90
  expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--pressed_false");
79
91
  expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--selected_false");
80
92
  });
@@ -4,6 +4,7 @@ import clsx from "clsx";
4
4
 
5
5
  import { useControllableState } from "../../hooks/useControllableState";
6
6
  import { usePressTap } from "../../hooks/usePressTap";
7
+ import { useScaleFeedback } from "../../hooks/useScaleFeedback";
7
8
  import type {
8
9
  LynxAccessibilityProps,
9
10
  LynxPressableProps,
@@ -12,6 +13,8 @@ import type {
12
13
  LynxViewRef,
13
14
  } from "../../types";
14
15
  import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
16
+ import { HStack } from "../Stack";
17
+ import { mergeProps } from "../../utils/merge-props";
15
18
 
16
19
  interface SegmentedControlContextValue {
17
20
  value: string | undefined;
@@ -105,8 +108,7 @@ export const SegmentedControlRoot = React.forwardRef<unknown, SegmentedControlRo
105
108
  <SegmentedControlContext.Provider value={contextValue}>
106
109
  <ClassNamesProvider value={classNames}>
107
110
  <view
108
- {...(ref ? { ref: ref as LynxViewRef } : {})}
109
- {...nativeProps}
111
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
110
112
  accessibility-element={accessibilityElement}
111
113
  accessibility-role-description={accessibilityRoleDescription}
112
114
  className={clsx(classNames.root, className)}
@@ -132,6 +134,7 @@ export interface SegmentedControlItemProps
132
134
  LynxAccessibilityProps,
133
135
  LynxPressableProps {
134
136
  children: string | number;
137
+ notification?: React.ReactNode;
135
138
  value: string;
136
139
  disabled?: boolean;
137
140
  }
@@ -140,6 +143,7 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
140
143
  (props, ref) => {
141
144
  const {
142
145
  children,
146
+ notification,
143
147
  className,
144
148
  style,
145
149
  value: itemValue,
@@ -170,11 +174,12 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
170
174
  [bindtap, context.selectValue, itemValue, selected],
171
175
  );
172
176
  const pressSelectionRef = React.useRef(selected);
173
- const { pressed, bindtouchstart, ...pressHandlers } = usePressTap({
174
- disabled,
175
- onTap: handleTap,
176
- mainThreadOnTap: mainThreadBindtap,
177
- });
177
+ const { pressed, bindtouchstart, bindtouchend, bindtouchcancel, ...pressHandlers } =
178
+ usePressTap({
179
+ disabled,
180
+ onTap: handleTap,
181
+ mainThreadOnTap: mainThreadBindtap,
182
+ });
178
183
  const handleTouchStart = React.useCallback(
179
184
  (...args: Parameters<typeof bindtouchstart>) => {
180
185
  pressSelectionRef.current = selected;
@@ -182,6 +187,12 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
182
187
  },
183
188
  [bindtouchstart, selected],
184
189
  );
190
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
191
+ disabled,
192
+ onTouchStart: handleTouchStart,
193
+ onTouchEnd: bindtouchend,
194
+ onTouchCancel: bindtouchcancel,
195
+ });
185
196
  const classes = segmentedControl({ selected, disabled, pressed });
186
197
  const pressStartClasses = segmentedControl({
187
198
  selected: pressSelectionRef.current,
@@ -193,10 +204,12 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
193
204
 
194
205
  return (
195
206
  <view
196
- {...(ref ? { ref: ref as LynxViewRef } : {})}
197
- {...nativeProps}
198
- bindtouchstart={handleTouchStart}
199
- {...pressHandlers}
207
+ {...mergeProps(
208
+ ref ? { ref: ref as LynxViewRef } : {},
209
+ scaleFeedbackTriggerProps,
210
+ pressHandlers,
211
+ nativeProps,
212
+ )}
200
213
  accessibility-element={accessibilityElement}
201
214
  accessibility-label={accessibilityLabel ?? label}
202
215
  accessibility-role-description={accessibilityRoleDescription}
@@ -208,7 +221,16 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
208
221
  style={style}
209
222
  >
210
223
  <view accessibility-elements-hidden={true} className={pressStartClasses.itemBackground} />
211
- <text className={classes.label}>{children}</text>
224
+ <view className={classes.itemContent} {...scaleFeedbackTargetProps}>
225
+ {notification ? (
226
+ <HStack position="relative" align="flex-start">
227
+ <text className={classes.label}>{children}</text>
228
+ <view accessibility-elements-hidden={true}>{notification}</view>
229
+ </HStack>
230
+ ) : (
231
+ <text className={classes.label}>{children}</text>
232
+ )}
233
+ </view>
212
234
  </view>
213
235
  );
214
236
  },
@@ -224,8 +246,7 @@ export const SegmentedControlIndicator = React.forwardRef<unknown, SegmentedCont
224
246
 
225
247
  return (
226
248
  <view
227
- {...(ref ? { ref: ref as LynxViewRef } : {})}
228
- {...nativeProps}
249
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
229
250
  accessibility-elements-hidden={true}
230
251
  className={clsx(classNames.indicator, className)}
231
252
  style={style}
@@ -1,5 +1,5 @@
1
1
  import "@testing-library/jest-dom";
2
- import { act, fireEvent, render } from "@lynx-js/react/testing-library";
2
+ import { act, fireEvent, render, waitSchedule } from "@lynx-js/react/testing-library";
3
3
  import { describe, expect, it, vi } from "vitest";
4
4
 
5
5
  import { RadioGroup } from "../RadioGroup";
@@ -29,6 +29,36 @@ function CheckItem() {
29
29
  }
30
30
 
31
31
  describe("SelectBox", () => {
32
+ it("shares checkbox touch state with content feedback and cancels without selecting", async () => {
33
+ const onCheckedChange = vi.fn();
34
+ const { container } = render(
35
+ <CheckSelectBox.Root onCheckedChange={onCheckedChange}>
36
+ <CheckSelectBox.Trigger>
37
+ <CheckSelectBox.Label>선택</CheckSelectBox.Label>
38
+ </CheckSelectBox.Trigger>
39
+ </CheckSelectBox.Root>,
40
+ { enableMainThread: true, enableBackgroundThread: true },
41
+ );
42
+ await waitSchedule();
43
+ const trigger = container.querySelector<HTMLElement>(".seed-select-box__interactionRoot")!;
44
+ const surface = container.querySelector(".seed-select-box__root")!;
45
+ const content = container.querySelector(".seed-select-box__scaleContent")!;
46
+ expect(content).toHaveAttribute("flatten", "false");
47
+ expect(content).not.toContainElement(surface.querySelector(".seed-select-box__selectedStroke"));
48
+
49
+ fireEvent.touchstart(trigger, {});
50
+ await waitSchedule();
51
+ expect(surface).toHaveClass("seed-select-box__root--pressed_true");
52
+ fireEvent.touchcancel(trigger, {});
53
+ await waitSchedule();
54
+ expect(surface).toHaveClass("seed-select-box__root--pressed_false");
55
+ expect(onCheckedChange).not.toHaveBeenCalled();
56
+
57
+ fireEvent.tap(trigger);
58
+ await waitSchedule();
59
+ expect(onCheckedChange).toHaveBeenCalledExactlyOnceWith(true);
60
+ });
61
+
32
62
  it("lays out custom label content in a view", () => {
33
63
  render(
34
64
  <CheckSelectBox.Root accessibility-label="Melon New">
@@ -137,6 +167,9 @@ describe("SelectBox", () => {
137
167
  <RadioSelectBox.Item value="second" accessibility-label="두 번째">
138
168
  <RadioSelectBox.Trigger>
139
169
  <RadioSelectBox.Label>두 번째</RadioSelectBox.Label>
170
+ <RadioGroup.ItemControl>
171
+ <RadioGroup.ItemIndicator />
172
+ </RadioGroup.ItemControl>
140
173
  </RadioSelectBox.Trigger>
141
174
  </RadioSelectBox.Item>
142
175
  <RadioSelectBox.Item value="disabled" accessibility-label="비활성" disabled>
@@ -153,6 +186,11 @@ describe("SelectBox", () => {
153
186
  ".seed-select-box__interactionRoot",
154
187
  );
155
188
  const surfaces = root.querySelectorAll<HTMLElement>(".seed-select-box__root");
189
+ const radioContent = surfaces[1].querySelector(".seed-select-box__scaleContent");
190
+ const radioMark = surfaces[1].querySelector(".seed-radiomark__root");
191
+
192
+ expect(radioContent).toHaveAttribute("flatten", "false");
193
+ expect(radioMark).not.toHaveAttribute("flatten", "false");
156
194
 
157
195
  expect(surfaces[0]).toHaveAttribute("accessibility-value", "selected");
158
196
  expect(surfaces[1]).toHaveAttribute("accessibility-value", "not selected");