@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
@@ -20,7 +20,6 @@ import {
20
20
  import {
21
21
  createContext,
22
22
  forwardRef,
23
- useCallback,
24
23
  useContext,
25
24
  useMemo,
26
25
  useRef,
@@ -40,6 +39,7 @@ import type {
40
39
  LynxViewRef,
41
40
  } from "../../types";
42
41
  import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
42
+ import { mergeProps } from "../../utils/merge-props";
43
43
 
44
44
  type BottomSheetClassNames = ReturnType<typeof bottomSheet>;
45
45
  type LynxForwardRefComponent<T, P> = ForwardRefExoticComponent<
@@ -153,15 +153,8 @@ export const BottomSheetRoot: LynxForwardRefComponent<SheetRootRef, BottomSheetR
153
153
 
154
154
  // `SheetRoot`에 직접 ref를 넘겨 Trigger 컨텍스트와 외부 ref에 같은 인스턴스를 동기화한다.
155
155
  // `useImperativeHandle([])`은 마운트 시점 값(null)을 고정시키는 함정이 있어 사용하지 않는다.
156
- const mergedRef = useCallback(
157
- (value: SheetRootRef | null) => {
158
- internalRef.current = value;
159
- if (typeof forwardedRef === "function") {
160
- forwardedRef(value);
161
- } else if (forwardedRef) {
162
- forwardedRef.current = value;
163
- }
164
- },
156
+ const mergedRef = useMemo(
157
+ () => mergeProps({ ref: internalRef }, { ref: forwardedRef }).ref,
165
158
  [forwardedRef],
166
159
  );
167
160
 
@@ -185,12 +178,10 @@ export const BottomSheetRoot: LynxForwardRefComponent<SheetRootRef, BottomSheetR
185
178
  <BottomSheetContext.Provider value={context}>
186
179
  <ClassNamesProvider value={classNames}>
187
180
  <SheetRoot
188
- ref={mergedRef}
181
+ {...mergeProps({ ref: mergedRef, onShowChange: onOpenChange }, nativeProps)}
189
182
  show={open}
190
183
  defaultShow={defaultOpen}
191
- onShowChange={onOpenChange}
192
184
  snapPoints={snapPoints ?? DEFAULT_SNAP_POINTS}
193
- {...nativeProps}
194
185
  >
195
186
  {children}
196
187
  </SheetRoot>
@@ -229,8 +220,10 @@ export const BottomSheetTrigger: LynxForwardRefComponent<unknown, BottomSheetTri
229
220
 
230
221
  return (
231
222
  <view
232
- {...(ref ? ({ ref: ref as LynxViewRef } as Record<string, unknown>) : {})}
233
- {...handlers}
223
+ {...mergeProps(
224
+ ref ? ({ ref: ref as LynxViewRef } as Record<string, unknown>) : {},
225
+ handlers,
226
+ )}
234
227
  className={className}
235
228
  style={style as never}
236
229
  >
@@ -296,8 +289,7 @@ export const BottomSheetContent: LynxForwardRefComponent<unknown, BottomSheetCon
296
289
 
297
290
  return (
298
291
  <SheetContent
299
- {...(ref ? { ref } : {})}
300
- {...restProps}
292
+ {...mergeProps(ref ? { ref } : {}, restProps)}
301
293
  className={clsx(classNames.content, className)}
302
294
  // `SheetContent` pins its outer view with an inline `left: 0`, which a recipe class
303
295
  // cannot outrank. Releasing it lets the positioner's `justify-content: center` place
@@ -3,6 +3,7 @@ import * as React from "@lynx-js/react";
3
3
 
4
4
  import type { LynxPressableProps, LynxStyledElementProps, LynxViewRef } from "../../types";
5
5
  import { useStyleProps, type StyleProps } from "../../utils/styled";
6
+ import { mergeProps } from "../../utils/merge-props";
6
7
 
7
8
  /**
8
9
  * @platform Lynx
@@ -26,8 +27,7 @@ export const Box = React.forwardRef<unknown, BoxProps>((props, ref) => {
26
27
 
27
28
  return (
28
29
  <view
29
- {...(ref ? { ref: ref as LynxViewRef } : {})}
30
- {...nativeProps}
30
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
31
31
  className={clsx(className)}
32
32
  style={style}
33
33
  >
@@ -5,6 +5,7 @@ import clsx from "clsx";
5
5
 
6
6
  import { useControllableState } from "../../hooks/useControllableState";
7
7
  import { usePressTap } from "../../hooks/usePressTap";
8
+ import { useScaleFeedback } from "../../hooks/useScaleFeedback";
8
9
  import type {
9
10
  LynxAccessibilityProps,
10
11
  LynxPressableProps,
@@ -15,6 +16,7 @@ import type {
15
16
  } from "../../types";
16
17
  import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
17
18
  import { IconSlotProvider } from "../Icon/Icon";
19
+ import { mergeProps } from "../../utils/merge-props";
18
20
 
19
21
  const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(callout);
20
22
 
@@ -46,7 +48,7 @@ function useCalloutContext(consumer: string) {
46
48
  * - 웹 focus ring은 지원하지 않습니다.
47
49
  */
48
50
  export interface CalloutRootProps
49
- extends Omit<CalloutVariantProps, "pressed">,
51
+ extends Omit<CalloutVariantProps, "pressed" | "interactive">,
50
52
  LynxStyledElementProps,
51
53
  LynxPressableProps,
52
54
  LynxAccessibilityProps {
@@ -75,12 +77,19 @@ export const CalloutRoot = React.forwardRef<unknown, CalloutRootProps>((props, r
75
77
  defaultValue: defaultOpen,
76
78
  });
77
79
  const isInteractive = bindtap != null || mainThreadBindtap != null;
78
- const pressTap = usePressTap({
80
+ const { pressed, bindtouchstart, bindtouchend, bindtouchcancel, ...pressTapHandlers } =
81
+ usePressTap({
82
+ disabled: !isInteractive,
83
+ onTap: bindtap,
84
+ mainThreadOnTap: mainThreadBindtap,
85
+ });
86
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
79
87
  disabled: !isInteractive,
80
- onTap: bindtap,
81
- mainThreadOnTap: mainThreadBindtap,
88
+ onTouchStart: bindtouchstart,
89
+ onTouchEnd: bindtouchend,
90
+ onTouchCancel: bindtouchcancel,
82
91
  });
83
- const classNames = callout({ ...variantProps, pressed: pressTap.pressed });
92
+ const classNames = callout({ ...variantProps, pressed, interactive: isInteractive });
84
93
  const dismiss = useMemoizedFn(() => {
85
94
  if (!open) return;
86
95
 
@@ -94,9 +103,9 @@ export const CalloutRoot = React.forwardRef<unknown, CalloutRootProps>((props, r
94
103
  prefixIcon: classNames.prefixIcon,
95
104
  suffixIcon: classNames.suffixIcon,
96
105
  },
97
- deps: [variantProps.tone ?? "neutral", pressTap.pressed],
106
+ deps: [variantProps.tone ?? "neutral", pressed],
98
107
  }),
99
- [classNames.prefixIcon, classNames.suffixIcon, pressTap.pressed, variantProps.tone],
108
+ [classNames.prefixIcon, classNames.suffixIcon, pressed, variantProps.tone],
100
109
  );
101
110
 
102
111
  if (!open) return null;
@@ -106,13 +115,18 @@ export const CalloutRoot = React.forwardRef<unknown, CalloutRootProps>((props, r
106
115
  <ClassNamesProvider value={classNames}>
107
116
  <IconSlotProvider value={iconSlotContextValue}>
108
117
  <view
109
- {...(ref ? { ref: ref as LynxViewRef } : {})}
110
- {...nativeProps}
111
- {...(isInteractive ? pressTap : {})}
118
+ {...mergeProps(
119
+ ref ? { ref: ref as LynxViewRef } : {},
120
+ isInteractive ? pressTapHandlers : {},
121
+ isInteractive ? scaleFeedbackTargetProps : {},
122
+ isInteractive ? scaleFeedbackTriggerProps : {},
123
+ nativeProps,
124
+ )}
112
125
  accessibility-element={accessibilityElement ?? (isInteractive ? true : undefined)}
113
126
  accessibility-traits={accessibilityTraits ?? (isInteractive ? "button" : undefined)}
114
127
  className={clsx(classNames.root, className)}
115
128
  style={style}
129
+ {...(isInteractive ? { flatten: false } : {})}
116
130
  >
117
131
  {children}
118
132
  </view>
@@ -133,8 +147,7 @@ export const CalloutContent = React.forwardRef<unknown, CalloutContentProps>((pr
133
147
 
134
148
  return (
135
149
  <text
136
- {...(ref ? { ref: ref as LynxTextRef } : {})}
137
- {...nativeProps}
150
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
138
151
  className={clsx(classNames.content, className)}
139
152
  style={style}
140
153
  >
@@ -154,8 +167,7 @@ export const CalloutTitle = React.forwardRef<unknown, CalloutTitleProps>((props,
154
167
 
155
168
  return (
156
169
  <text
157
- {...(ref ? { ref: ref as LynxTextRef } : {})}
158
- {...nativeProps}
170
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
159
171
  className={clsx(classNames.title, className)}
160
172
  style={style}
161
173
  >
@@ -177,8 +189,7 @@ export const CalloutDescription = React.forwardRef<unknown, CalloutDescriptionPr
177
189
 
178
190
  return (
179
191
  <text
180
- {...(ref ? { ref: ref as LynxTextRef } : {})}
181
- {...nativeProps}
192
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
182
193
  className={clsx(classNames.description, className)}
183
194
  style={style}
184
195
  >
@@ -210,8 +221,7 @@ export const CalloutLink = React.forwardRef<unknown, CalloutLinkProps>((props, r
210
221
 
211
222
  return (
212
223
  <text
213
- {...(ref ? { ref: ref as LynxTextRef } : {})}
214
- {...nativeProps}
224
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
215
225
  accessibility-element={accessibilityElement}
216
226
  accessibility-traits={accessibilityTraits}
217
227
  className={clsx(classNames.link, className)}
@@ -226,7 +236,8 @@ CalloutLink.displayName = "CalloutLink";
226
236
  ////////////////////////////////////////////////////////////////////////////////////
227
237
 
228
238
  export interface CalloutCloseButtonProps
229
- extends LynxStyledElementProps,
239
+ // Keep the scale target's Android View even if shared props later expose flatten.
240
+ extends Omit<LynxStyledElementProps, "flatten">,
230
241
  LynxPressableProps,
231
242
  LynxAccessibilityProps {}
232
243
 
@@ -248,6 +259,7 @@ export const CalloutCloseButton = React.forwardRef<unknown, CalloutCloseButtonPr
248
259
  bindtap?.(event);
249
260
  dismiss();
250
261
  });
262
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback();
251
263
 
252
264
  if (process.env.NODE_ENV !== "production" && accessibilityElement && !accessibilityLabel) {
253
265
  console.warn("CalloutCloseButton requires `accessibility-label` for accessibility.");
@@ -255,14 +267,19 @@ export const CalloutCloseButton = React.forwardRef<unknown, CalloutCloseButtonPr
255
267
 
256
268
  return (
257
269
  <view
258
- {...(ref ? { ref: ref as LynxViewRef } : {})}
259
- {...nativeProps}
260
- bindtap={handleTap}
270
+ {...mergeProps(
271
+ { bindtap: handleTap },
272
+ ref ? { ref: ref as LynxViewRef } : {},
273
+ scaleFeedbackTargetProps,
274
+ scaleFeedbackTriggerProps,
275
+ nativeProps,
276
+ )}
261
277
  accessibility-element={accessibilityElement}
262
278
  accessibility-label={accessibilityLabel}
263
279
  accessibility-traits={accessibilityTraits}
264
280
  className={clsx(classNames.closeButton, className)}
265
281
  style={style}
282
+ flatten={false}
266
283
  >
267
284
  {children}
268
285
  </view>
@@ -79,6 +79,50 @@ function getCheckboxBackground() {
79
79
  }
80
80
 
81
81
  describe("Checkbox", () => {
82
+ it("exposes checkbox accessibility state", () => {
83
+ const { rerender } = render(<Checkbox.Root />);
84
+
85
+ expect(getCheckboxRoot()).toHaveAttribute("accessibility-element", "true");
86
+ expect(getCheckboxRoot()).toHaveAttribute("accessibility-role-description", "checkbox");
87
+ expect(getCheckboxRoot()).toHaveAttribute("accessibility-value", "not checked");
88
+ expect(getCheckboxRoot()).not.toHaveAttribute("accessibility-traits");
89
+
90
+ rerender(<Checkbox.Root checked disabled />);
91
+
92
+ expect(getCheckboxRoot()).toHaveAttribute("accessibility-value", "checked");
93
+ expect(getCheckboxRoot()).toHaveAttribute("accessibility-traits", "disabled");
94
+ });
95
+
96
+ it("prefers explicit accessibility props over defaults", () => {
97
+ render(
98
+ <Checkbox.Root
99
+ checked
100
+ disabled
101
+ accessibility-element={false}
102
+ accessibility-role-description="custom checkbox"
103
+ accessibility-value="custom value"
104
+ accessibility-traits="button"
105
+ />,
106
+ );
107
+
108
+ const root = getCheckboxRoot();
109
+
110
+ expect(root).toHaveAttribute("accessibility-element", "false");
111
+ expect(root).toHaveAttribute("accessibility-role-description", "custom checkbox");
112
+ expect(root).toHaveAttribute("accessibility-value", "custom value");
113
+ expect(root).toHaveAttribute("accessibility-traits", "button");
114
+ });
115
+
116
+ it("applies the checkbox size class to the control", () => {
117
+ render(
118
+ <Checkbox.Root size="large">
119
+ <Checkbox.Control />
120
+ </Checkbox.Root>,
121
+ );
122
+
123
+ expect(getCheckboxControl()).toHaveClass("seed-checkbox__control--size_large");
124
+ });
125
+
82
126
  it("keeps each ghost press color stable through release in both toggle directions", () => {
83
127
  const onCheckedChange = vi.fn();
84
128
 
@@ -9,8 +9,11 @@ import type { CheckmarkVariantProps } from "@seed-design/lynx-css/recipes/checkm
9
9
  import { checkboxGroup } from "@seed-design/lynx-css/recipes/checkbox-group";
10
10
 
11
11
  import { useControllableState } from "../../hooks/useControllableState";
12
+ import { ScaleFeedbackContentContext } from "../../contexts";
12
13
  import { usePressTap } from "../../hooks/usePressTap";
14
+ import { useScaleFeedback, type ScaleFeedbackTargetProps } from "../../hooks/useScaleFeedback";
13
15
  import type {
16
+ LynxAccessibilityProps,
14
17
  LynxIconElementProps,
15
18
  LynxStyledElementProps,
16
19
  LynxTextRef,
@@ -18,6 +21,7 @@ import type {
18
21
  } from "../../types";
19
22
  import { splitMultipleVariantsProps } from "../../utils/split-multiple-variants-props";
20
23
  import { InternalIcon } from "../Icon/Icon";
24
+ import { mergeProps } from "../../utils/merge-props";
21
25
 
22
26
  /**
23
27
  * @platform Lynx
@@ -43,6 +47,7 @@ interface CheckboxContextValue {
43
47
  checkboxVariantProps: CheckboxVariantProps;
44
48
  checkmarkVariantProps: CheckmarkVariantProps;
45
49
  toggle: () => void;
50
+ scaleFeedbackTargetProps: ScaleFeedbackTargetProps;
46
51
  }
47
52
 
48
53
  const CheckboxContext = React.createContext<CheckboxContextValue | null>(null);
@@ -75,7 +80,8 @@ function useCheckmarkControlContext(consumer: string): CheckmarkControlContextVa
75
80
  export interface CheckboxRootProps
76
81
  extends CheckboxVariantProps,
77
82
  Omit<CheckmarkVariantProps, "size" | "checked" | "disabled" | "indeterminate">,
78
- LynxStyledElementProps {
83
+ LynxStyledElementProps,
84
+ LynxAccessibilityProps {
79
85
  checked?: boolean;
80
86
  defaultChecked?: boolean;
81
87
  indeterminate?: boolean;
@@ -94,8 +100,15 @@ export const CheckboxRoot = React.forwardRef<unknown, CheckboxRootProps>((props,
94
100
  onCheckedChange,
95
101
  ...restProps
96
102
  } = props;
97
- const [{ checkbox: checkboxVariantProps, checkmark: checkmarkVariantProps }, nativeProps] =
103
+ const [{ checkbox: checkboxVariantProps, checkmark: checkmarkVariantProps }, restNativeProps] =
98
104
  splitMultipleVariantsProps(restProps, { checkbox, checkmark });
105
+ const {
106
+ "accessibility-element": accessibilityElement = true,
107
+ "accessibility-role-description": accessibilityRoleDescription = "checkbox",
108
+ "accessibility-traits": accessibilityTraits,
109
+ "accessibility-value": accessibilityValue,
110
+ ...nativeProps
111
+ } = restNativeProps;
99
112
 
100
113
  const [checked, setChecked] = useControllableState({
101
114
  value: checkedProp,
@@ -106,17 +119,20 @@ export const CheckboxRoot = React.forwardRef<unknown, CheckboxRootProps>((props,
106
119
  const toggle = React.useCallback(() => setChecked(!checked), [checked, setChecked]);
107
120
 
108
121
  const pressSelectionRef = React.useRef({ checked, indeterminate });
109
- const { pressed, bindtouchstart, ...pressHandlers } = usePressTap({
122
+ const { pressed, bindtouchstart, bindtouchend, bindtouchcancel, ...pressHandlers } = usePressTap({
110
123
  disabled,
111
124
  onTap: toggle,
112
125
  });
113
- const handleTouchStart = React.useCallback(
114
- (...args: Parameters<typeof bindtouchstart>) => {
115
- pressSelectionRef.current = { checked, indeterminate };
116
- bindtouchstart(...args);
117
- },
118
- [bindtouchstart, checked, indeterminate],
119
- );
126
+ const handlePressStart = React.useCallback(() => {
127
+ pressSelectionRef.current = { checked, indeterminate };
128
+ bindtouchstart();
129
+ }, [bindtouchstart, checked, indeterminate]);
130
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
131
+ disabled,
132
+ onTouchStart: handlePressStart,
133
+ onTouchEnd: bindtouchend,
134
+ onTouchCancel: bindtouchcancel,
135
+ });
120
136
 
121
137
  const rootClassName = checkbox({ ...checkboxVariantProps, disabled }).root;
122
138
 
@@ -131,6 +147,7 @@ export const CheckboxRoot = React.forwardRef<unknown, CheckboxRootProps>((props,
131
147
  checkboxVariantProps,
132
148
  checkmarkVariantProps,
133
149
  toggle,
150
+ scaleFeedbackTargetProps,
134
151
  }),
135
152
  [
136
153
  checked,
@@ -140,17 +157,24 @@ export const CheckboxRoot = React.forwardRef<unknown, CheckboxRootProps>((props,
140
157
  checkboxVariantProps,
141
158
  checkmarkVariantProps,
142
159
  toggle,
160
+ scaleFeedbackTargetProps,
143
161
  ],
144
162
  );
145
163
 
146
164
  return (
147
165
  <CheckboxContext.Provider value={contextValue}>
148
166
  <view
149
- {...(ref ? { ref: ref as LynxViewRef } : {})}
167
+ {...mergeProps(
168
+ ref ? { ref: ref as LynxViewRef } : {},
169
+ scaleFeedbackTriggerProps,
170
+ pressHandlers,
171
+ nativeProps,
172
+ )}
150
173
  className={clsx(rootClassName, className)}
151
- bindtouchstart={handleTouchStart}
152
- {...pressHandlers}
153
- {...nativeProps}
174
+ accessibility-element={accessibilityElement}
175
+ accessibility-role-description={accessibilityRoleDescription}
176
+ accessibility-traits={accessibilityTraits ?? (disabled ? "disabled" : undefined)}
177
+ accessibility-value={accessibilityValue ?? (checked ? "checked" : "not checked")}
154
178
  >
155
179
  {children}
156
180
  </view>
@@ -169,6 +193,7 @@ export const CheckboxControl = React.forwardRef<unknown, CheckboxControlProps>((
169
193
  const [variantProps, restProps] = checkmark.splitVariantProps(props);
170
194
  const { children, className, ...nativeProps } = restProps;
171
195
  const context = useCheckboxContext("CheckboxControl");
196
+ const hasScaledContent = React.useContext(ScaleFeedbackContentContext);
172
197
  const checkmarkVariantProps: CheckmarkVariantProps = {
173
198
  ...context.checkmarkVariantProps,
174
199
  ...variantProps,
@@ -178,6 +203,10 @@ export const CheckboxControl = React.forwardRef<unknown, CheckboxControlProps>((
178
203
  pressed: context.pressed,
179
204
  };
180
205
  const classes = checkmark(checkmarkVariantProps);
206
+ const checkboxControlClassName = checkbox({
207
+ ...context.checkboxVariantProps,
208
+ disabled: context.disabled,
209
+ }).control;
181
210
 
182
211
  // Lynx는 opaque color와 transparent black 사이의 background-color를 보간할 때
183
212
  // 중간 RGB가 검게 탁해진다. ghost root는 투명 상태로 고정하고 별도 배경의
@@ -204,9 +233,13 @@ export const CheckboxControl = React.forwardRef<unknown, CheckboxControlProps>((
204
233
  value={{ iconClassName: classes.icon, checkmarkVariantProps }}
205
234
  >
206
235
  <view
207
- {...(ref ? { ref: ref as LynxViewRef } : {})}
208
- className={clsx(rootClassName, className)}
209
- {...nativeProps}
236
+ {...mergeProps(
237
+ ref ? { ref: ref as LynxViewRef } : {},
238
+ !hasScaledContent ? context.scaleFeedbackTargetProps : {},
239
+ nativeProps,
240
+ )}
241
+ className={clsx(checkboxControlClassName, rootClassName, className)}
242
+ {...(!hasScaledContent ? { flatten: false } : {})}
210
243
  >
211
244
  {isGhost ? <view className={pressStartClasses.background} /> : null}
212
245
  {children}
@@ -285,9 +318,8 @@ export const CheckboxLabel = React.forwardRef<unknown, CheckboxLabelProps>((prop
285
318
 
286
319
  return (
287
320
  <text
288
- {...(ref ? { ref: ref as LynxTextRef } : {})}
321
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
289
322
  className={clsx(labelClassName, className)}
290
- {...nativeProps}
291
323
  >
292
324
  {children}
293
325
  </text>
@@ -305,9 +337,8 @@ export const CheckboxGroup = React.forwardRef<unknown, CheckboxGroupProps>((prop
305
337
 
306
338
  return (
307
339
  <view
308
- {...(ref ? { ref: ref as LynxViewRef } : {})}
340
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
309
341
  className={clsx(classes.root, className)}
310
- {...nativeProps}
311
342
  >
312
343
  {children}
313
344
  </view>
@@ -38,6 +38,7 @@ describe("Chip", () => {
38
38
 
39
39
  expect(root).toHaveClass("seed-chip__root--size_large");
40
40
  expect(root).toHaveClass("seed-chip__root--variant_outlineStrong");
41
+ expect(root).toHaveAttribute("accessibility-role-description", "button");
41
42
  expect(label).toHaveTextContent("버튼 칩");
42
43
  expect(label).toHaveClass("seed-chip__label--size_large");
43
44
  });
@@ -52,12 +53,15 @@ describe("Chip", () => {
52
53
 
53
54
  const root = getChipRoot();
54
55
  expect(root).toHaveClass("seed-chip__root--selected_false");
56
+ expect(root).toHaveAttribute("accessibility-role-description", "toggle");
57
+ expect(root).toHaveAttribute("accessibility-value", "not checked");
55
58
 
56
59
  fireEvent.tap(root);
57
60
 
58
61
  expect(onCheckedChange).toHaveBeenCalledWith(true);
59
62
  expect(getChipRoot()).toHaveClass("seed-chip__root--selected_true");
60
- expect(getChipRoot()).toHaveAttribute("accessibility-traits", "selected");
63
+ expect(getChipRoot()).toHaveAttribute("accessibility-role-description", "toggle");
64
+ expect(getChipRoot()).toHaveAttribute("accessibility-value", "checked");
61
65
  });
62
66
 
63
67
  it("does not toggle when disabled", () => {
@@ -92,6 +96,10 @@ describe("Chip", () => {
92
96
  const roots = getRenderedRoot().querySelectorAll(".seed-chip__root");
93
97
  expect(roots[0]).toHaveClass("seed-chip__root--selected_true");
94
98
  expect(roots[1]).toHaveClass("seed-chip__root--selected_false");
99
+ expect(roots[0]).toHaveAttribute("accessibility-role-description", "radio");
100
+ expect(roots[0]).toHaveAttribute("accessibility-value", "selected");
101
+ expect(roots[1]).toHaveAttribute("accessibility-role-description", "radio");
102
+ expect(roots[1]).toHaveAttribute("accessibility-value", "not selected");
95
103
 
96
104
  fireEvent.tap(roots[1] as HTMLElement);
97
105
 
@@ -99,6 +107,47 @@ describe("Chip", () => {
99
107
  const updatedRoots = getRenderedRoot().querySelectorAll(".seed-chip__root");
100
108
  expect(updatedRoots[0]).toHaveClass("seed-chip__root--selected_false");
101
109
  expect(updatedRoots[1]).toHaveClass("seed-chip__root--selected_true");
110
+ expect(updatedRoots[0]).toHaveAttribute("accessibility-role-description", "radio");
111
+ expect(updatedRoots[0]).toHaveAttribute("accessibility-value", "not selected");
112
+ expect(updatedRoots[1]).toHaveAttribute("accessibility-role-description", "radio");
113
+ expect(updatedRoots[1]).toHaveAttribute("accessibility-value", "selected");
114
+ });
115
+
116
+ it("prefers custom accessibility props to defaults", () => {
117
+ render(
118
+ <view>
119
+ <Chip.Button
120
+ disabled
121
+ accessibility-role-description="custom button"
122
+ accessibility-traits="link"
123
+ />
124
+ <Chip.Toggle
125
+ disabled
126
+ defaultChecked
127
+ accessibility-role-description="custom toggle"
128
+ accessibility-traits="link"
129
+ accessibility-value="custom checked"
130
+ />
131
+ <Chip.RadioRoot defaultValue="selected" disabled>
132
+ <Chip.RadioItem
133
+ value="selected"
134
+ accessibility-role-description="custom radio"
135
+ accessibility-traits="link"
136
+ accessibility-value="custom selected"
137
+ />
138
+ </Chip.RadioRoot>
139
+ </view>,
140
+ );
141
+
142
+ const roots = getRenderedRoot().querySelectorAll(".seed-chip__root");
143
+ expect(roots[0]).toHaveAttribute("accessibility-role-description", "custom button");
144
+ expect(roots[0]).toHaveAttribute("accessibility-traits", "link");
145
+ expect(roots[1]).toHaveAttribute("accessibility-role-description", "custom toggle");
146
+ expect(roots[1]).toHaveAttribute("accessibility-traits", "link");
147
+ expect(roots[1]).toHaveAttribute("accessibility-value", "custom checked");
148
+ expect(roots[2]).toHaveAttribute("accessibility-role-description", "custom radio");
149
+ expect(roots[2]).toHaveAttribute("accessibility-traits", "link");
150
+ expect(roots[2]).toHaveAttribute("accessibility-value", "custom selected");
102
151
  });
103
152
 
104
153
  it("requires radio items to be rendered inside a radio root", () => {