@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,57 @@
1
+ import "@testing-library/jest-dom";
2
+ import { fireEvent, render } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { Switch } from "./index";
6
+
7
+ function getSwitchRoot(container: HTMLElement) {
8
+ const root = container.querySelector<HTMLElement>(".seed-switch__root");
9
+
10
+ if (!root) {
11
+ throw new Error("Expected Switch root to exist.");
12
+ }
13
+
14
+ return root;
15
+ }
16
+
17
+ describe("Switch", () => {
18
+ it("exposes switch accessibility state and preserves toggling", () => {
19
+ const onCheckedChange = vi.fn();
20
+ const { container } = render(
21
+ <Switch.Root accessibility-label="알림" onCheckedChange={onCheckedChange}>
22
+ <Switch.Control>
23
+ <Switch.Thumb />
24
+ </Switch.Control>
25
+ </Switch.Root>,
26
+ );
27
+
28
+ const root = getSwitchRoot(container);
29
+
30
+ expect(root).toHaveAttribute("accessibility-element", "true");
31
+ expect(root).toHaveAttribute("accessibility-role-description", "switch");
32
+ expect(root).toHaveAttribute("accessibility-label", "알림");
33
+ expect(root).toHaveAttribute("accessibility-value", "not checked");
34
+
35
+ fireEvent.tap(root);
36
+
37
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
38
+ expect(root).toHaveAttribute("accessibility-value", "checked");
39
+ });
40
+
41
+ it("exposes the disabled state and ignores taps", () => {
42
+ const onCheckedChange = vi.fn();
43
+ const { container } = render(
44
+ <Switch.Root defaultChecked disabled onCheckedChange={onCheckedChange} />,
45
+ );
46
+
47
+ const root = getSwitchRoot(container);
48
+
49
+ expect(root).toHaveAttribute("accessibility-value", "checked");
50
+ expect(root).toHaveAttribute("accessibility-traits", "disabled");
51
+
52
+ fireEvent.tap(root);
53
+
54
+ expect(onCheckedChange).not.toHaveBeenCalled();
55
+ expect(root).toHaveAttribute("accessibility-value", "checked");
56
+ });
57
+ });
@@ -9,7 +9,17 @@ import type { SwitchmarkVariantProps } from "@seed-design/lynx-css/recipes/switc
9
9
  import { splitMultipleVariantsProps } from "../../utils/split-multiple-variants-props";
10
10
  import { useControllableState } from "../../hooks/useControllableState";
11
11
  import { usePressTap } from "../../hooks/usePressTap";
12
- import type { LynxStyledElementProps, LynxTextRef, LynxViewRef } from "../../types";
12
+ import type {
13
+ LynxAccessibilityProps,
14
+ LynxStyledElementProps,
15
+ LynxTextRef,
16
+ LynxViewRef,
17
+ } from "../../types";
18
+ import {
19
+ useScaleFeedback,
20
+ type ScaleFeedbackTargetProps,
21
+ } from "../../hooks/useScaleFeedback";
22
+ import { mergeProps } from "../../utils/merge-props";
13
23
 
14
24
  /**
15
25
  * @platform Lynx
@@ -27,14 +37,16 @@ import type { LynxStyledElementProps, LynxTextRef, LynxViewRef } from "../../typ
27
37
  interface SwitchContextValue {
28
38
  checked: boolean;
29
39
  disabled: boolean;
40
+ pressed: boolean;
30
41
  switchVariantProps: SwitchVariantProps;
31
42
  switchmarkVariantProps: SwitchmarkVariantProps;
32
43
  toggle: () => void;
44
+ scaleFeedbackTargetProps: ScaleFeedbackTargetProps;
33
45
  }
34
46
 
35
47
  const SwitchContext = React.createContext<SwitchContextValue | null>(null);
36
48
 
37
- function useSwitchContext(consumer: string): SwitchContextValue {
49
+ export function useSwitchContext(consumer: string): SwitchContextValue {
38
50
  const ctx = React.useContext(SwitchContext);
39
51
  if (!ctx) {
40
52
  throw new Error(`<${consumer}/> must be rendered inside <SwitchRoot/>.`);
@@ -62,7 +74,8 @@ function useSwitchmarkControlContext(consumer: string): SwitchmarkControlContext
62
74
  export interface SwitchRootProps
63
75
  extends SwitchVariantProps,
64
76
  Omit<SwitchmarkVariantProps, "size">,
65
- LynxStyledElementProps {
77
+ LynxStyledElementProps,
78
+ LynxAccessibilityProps {
66
79
  checked?: boolean;
67
80
  defaultChecked?: boolean;
68
81
  disabled?: boolean;
@@ -79,8 +92,15 @@ export const SwitchRoot = React.forwardRef<unknown, SwitchRootProps>((props, ref
79
92
  onCheckedChange,
80
93
  ...restProps
81
94
  } = props;
82
- const [{ switch: switchVariantProps, switchmark: switchmarkVariantProps }, nativeProps] =
95
+ const [{ switch: switchVariantProps, switchmark: switchmarkVariantProps }, restNativeProps] =
83
96
  splitMultipleVariantsProps(restProps, { switch: switchStyle, switchmark });
97
+ const {
98
+ "accessibility-element": accessibilityElement = true,
99
+ "accessibility-role-description": accessibilityRoleDescription = "switch",
100
+ "accessibility-traits": accessibilityTraits,
101
+ "accessibility-value": accessibilityValue,
102
+ ...nativeProps
103
+ } = restNativeProps;
84
104
 
85
105
  const [checked, setChecked] = useControllableState({
86
106
  value: checkedProp,
@@ -90,10 +110,22 @@ export const SwitchRoot = React.forwardRef<unknown, SwitchRootProps>((props, ref
90
110
 
91
111
  const toggle = React.useCallback(() => setChecked(!checked), [checked, setChecked]);
92
112
 
93
- const { pressed: _pressed, ...pressHandlers } = usePressTap({
113
+ const {
114
+ pressed,
115
+ bindtouchstart,
116
+ bindtouchend,
117
+ bindtouchcancel,
118
+ ...pressHandlers
119
+ } = usePressTap({
94
120
  disabled,
95
121
  onTap: toggle,
96
122
  });
123
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
124
+ disabled,
125
+ onTouchStart: bindtouchstart,
126
+ onTouchEnd: bindtouchend,
127
+ onTouchCancel: bindtouchcancel,
128
+ });
97
129
 
98
130
  const rootClassName = switchStyle({ ...switchVariantProps, disabled }).root;
99
131
 
@@ -101,20 +133,29 @@ export const SwitchRoot = React.forwardRef<unknown, SwitchRootProps>((props, ref
101
133
  () => ({
102
134
  checked,
103
135
  disabled,
136
+ pressed,
104
137
  switchVariantProps,
105
138
  switchmarkVariantProps,
106
139
  toggle,
140
+ scaleFeedbackTargetProps,
107
141
  }),
108
- [checked, disabled, switchVariantProps, switchmarkVariantProps, toggle],
142
+ [checked, disabled, pressed, switchVariantProps, switchmarkVariantProps, toggle, scaleFeedbackTargetProps],
109
143
  );
110
144
 
111
145
  return (
112
146
  <SwitchContext.Provider value={contextValue}>
113
147
  <view
114
- {...(ref ? { ref: ref as LynxViewRef } : {})}
148
+ {...mergeProps(
149
+ ref ? { ref: ref as LynxViewRef } : {},
150
+ scaleFeedbackTriggerProps,
151
+ pressHandlers,
152
+ nativeProps,
153
+ )}
115
154
  className={clsx(rootClassName, className)}
116
- {...pressHandlers}
117
- {...nativeProps}
155
+ accessibility-element={accessibilityElement}
156
+ accessibility-role-description={accessibilityRoleDescription}
157
+ accessibility-traits={accessibilityTraits ?? (disabled ? "disabled" : undefined)}
158
+ accessibility-value={accessibilityValue ?? (checked ? "checked" : "not checked")}
118
159
  >
119
160
  {children}
120
161
  </view>
@@ -127,7 +168,8 @@ SwitchRoot.displayName = "SwitchRoot";
127
168
 
128
169
  export interface SwitchControlProps
129
170
  extends Pick<SwitchmarkVariantProps, "tone" | "size">,
130
- LynxStyledElementProps {}
171
+ // Keep the scale target's Android View even if shared props later expose flatten.
172
+ Omit<LynxStyledElementProps, "flatten"> {}
131
173
 
132
174
  export const SwitchControl = React.forwardRef<unknown, SwitchControlProps>((props, ref) => {
133
175
  const [variantProps, restProps] = switchmark.splitVariantProps(props);
@@ -146,9 +188,13 @@ export const SwitchControl = React.forwardRef<unknown, SwitchControlProps>((prop
146
188
  value={{ thumbClassName: classes.thumb, switchmarkVariantProps }}
147
189
  >
148
190
  <view
149
- {...(ref ? { ref: ref as LynxViewRef } : {})}
191
+ {...mergeProps(
192
+ ref ? { ref: ref as LynxViewRef } : {},
193
+ context.scaleFeedbackTargetProps,
194
+ nativeProps,
195
+ )}
150
196
  className={clsx(classes.root, className)}
151
- {...nativeProps}
197
+ flatten={false}
152
198
  >
153
199
  {children}
154
200
  </view>
@@ -167,9 +213,8 @@ export const SwitchThumb = React.forwardRef<unknown, SwitchThumbProps>((props, r
167
213
 
168
214
  return (
169
215
  <view
170
- {...(ref ? { ref: ref as LynxViewRef } : {})}
216
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
171
217
  className={clsx(thumbClassName, className)}
172
- {...nativeProps}
173
218
  />
174
219
  );
175
220
  });
@@ -189,9 +234,8 @@ export const SwitchLabel = React.forwardRef<unknown, SwitchLabelProps>((props, r
189
234
 
190
235
  return (
191
236
  <text
192
- {...(ref ? { ref: ref as LynxTextRef } : {})}
237
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
193
238
  className={clsx(labelClassName, className)}
194
- {...nativeProps}
195
239
  >
196
240
  {children}
197
241
  </text>
@@ -82,6 +82,25 @@ describe("Tabs", () => {
82
82
  expect(second).toHaveAttribute("accessibility-value", "selected");
83
83
  });
84
84
 
85
+ it("calls bindtap when the selected trigger is tapped again", () => {
86
+ const bindtap = vi.fn();
87
+ const onValueChange = vi.fn();
88
+ const { container } = render(
89
+ <Tabs.Root defaultValue="one" onValueChange={onValueChange}>
90
+ <Tabs.List>
91
+ <Tabs.Trigger value="one" bindtap={bindtap}>
92
+ 첫 번째
93
+ </Tabs.Trigger>
94
+ </Tabs.List>
95
+ </Tabs.Root>,
96
+ );
97
+
98
+ fireEvent.tap(getTrigger(container, "첫 번째"));
99
+
100
+ expect(bindtap).toHaveBeenCalledTimes(1);
101
+ expect(onValueChange).not.toHaveBeenCalled();
102
+ });
103
+
85
104
  it("derives trigger positions from ordered widths instead of platform coordinates", () => {
86
105
  expect(getTabsLayoutWidth({ detail: { width: 128 }, params: { width: 128 } })).toBe(128);
87
106
  expect(
@@ -12,13 +12,16 @@ import clsx from "clsx";
12
12
 
13
13
  import { useControllableState } from "../../hooks/useControllableState";
14
14
  import { usePressTap } from "../../hooks/usePressTap";
15
+ import { useScaleFeedback } from "../../hooks/useScaleFeedback";
15
16
  import type {
16
17
  LynxAccessibilityProps,
18
+ LynxPressableProps,
17
19
  LynxStyledElementProps,
18
20
  LynxViewProps,
19
21
  LynxViewRef,
20
22
  } from "../../types";
21
23
  import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
24
+ import { Box } from "../Box";
22
25
  import {
23
26
  areTabsTransitionsEnabled,
24
27
  getTabsLayoutWidth,
@@ -26,6 +29,7 @@ import {
26
29
  getTabsTriggerRects,
27
30
  type TabsLayoutRect,
28
31
  } from "./Tabs.utils";
32
+ import { mergeProps } from "../../utils/merge-props";
29
33
 
30
34
  type NativeViewProps = IntrinsicElements["view"];
31
35
  type NativeViewPagerProps = IntrinsicElements["viewpager"];
@@ -333,8 +337,7 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
333
337
  <TabsContext.Provider value={contextValue}>
334
338
  <ClassNamesProvider value={classNames}>
335
339
  <view
336
- {...(ref ? { ref: ref as LynxViewRef } : {})}
337
- {...nativeProps}
340
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
338
341
  className={clsx(classNames.root, className)}
339
342
  style={style}
340
343
  >
@@ -381,19 +384,14 @@ export const TabsList = React.forwardRef<unknown, TabsListProps>((props, ref) =>
381
384
  syncTriggerOrder(triggerOrder);
382
385
  }, [items, syncTriggerOrder, triggerOrder]);
383
386
 
384
- const mergedRef = React.useCallback(
385
- (node: NodesRef | null) => {
386
- setListRef(node);
387
- if (typeof ref === "function") ref(node);
388
- else if (ref) ref.current = node;
389
- },
387
+ const mergedRef = React.useMemo(
388
+ () => mergeProps({ ref: setListRef }, { ref: ref as LynxViewRef }).ref,
390
389
  [ref, setListRef],
391
390
  );
392
391
 
393
392
  return (
394
393
  <scroll-view
395
- ref={mergedRef}
396
- {...nativeProps}
394
+ {...mergeProps({ ref: mergedRef }, nativeProps)}
397
395
  scroll-orientation="horizontal"
398
396
  scroll-bar-enable={false}
399
397
  accessibility-element={false}
@@ -409,19 +407,26 @@ TabsList.displayName = "TabsList";
409
407
 
410
408
  ////////////////////////////////////////////////////////////////////////////////////
411
409
 
412
- export interface TabsTriggerProps extends LynxStyledElementProps {
410
+ // Keep the scale target's Android View even if shared props later expose flatten.
411
+ export interface TabsTriggerProps
412
+ extends Omit<LynxStyledElementProps, "children" | "flatten">,
413
+ Pick<LynxPressableProps, "bindtap"> {
414
+ children: string | number;
413
415
  value: string;
414
416
  disabled?: boolean;
417
+ notification?: React.ReactNode;
415
418
  "accessibility-label"?: LynxAccessibilityProps["accessibility-label"];
416
419
  }
417
420
 
418
421
  export const TabsTrigger = React.forwardRef<unknown, TabsTriggerProps>((props, ref) => {
419
422
  const {
420
423
  children,
424
+ notification,
421
425
  className,
422
426
  style,
423
427
  value: triggerValue,
424
428
  disabled = false,
429
+ bindtap,
425
430
  "accessibility-label": accessibilityLabel,
426
431
  ...nativeProps
427
432
  } = props;
@@ -439,11 +444,27 @@ export const TabsTrigger = React.forwardRef<unknown, TabsTriggerProps>((props, r
439
444
  context.updateTriggerDisabled(triggerValue, disabled);
440
445
  }, [context.updateTriggerDisabled, triggerValue, disabled]);
441
446
 
442
- const handleTap = React.useCallback<NonNullable<NativeViewProps["bindtap"]>>(() => {
443
- "background only";
444
- context.selectValue(triggerValue);
445
- }, [context.selectValue, triggerValue]);
446
- const { pressed: _pressed, ...pressHandlers } = usePressTap({ disabled, onTap: handleTap });
447
+ const handleTap = React.useCallback<NonNullable<NativeViewProps["bindtap"]>>(
448
+ (...args) => {
449
+ "background only";
450
+ bindtap?.(...args);
451
+ context.selectValue(triggerValue);
452
+ },
453
+ [bindtap, context.selectValue, triggerValue],
454
+ );
455
+ const {
456
+ pressed: _pressed,
457
+ bindtouchstart,
458
+ bindtouchend,
459
+ bindtouchcancel,
460
+ ...pressHandlers
461
+ } = usePressTap({ disabled, onTap: handleTap });
462
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
463
+ disabled,
464
+ onTouchStart: bindtouchstart,
465
+ onTouchEnd: bindtouchend,
466
+ onTouchCancel: bindtouchcancel,
467
+ });
447
468
 
448
469
  const handleLayoutChange = React.useCallback<LayoutChangeHandler>(
449
470
  (...args) => {
@@ -465,11 +486,15 @@ export const TabsTrigger = React.forwardRef<unknown, TabsTriggerProps>((props, r
465
486
 
466
487
  return (
467
488
  <view
468
- {...(ref ? { ref: ref as LynxViewRef } : {})}
469
- {...nativeProps}
470
- {...pressHandlers}
489
+ {...mergeProps(
490
+ { bindlayoutchange: handleLayoutChange },
491
+ ref ? { ref: ref as LynxViewRef } : {},
492
+ pressHandlers,
493
+ scaleFeedbackTargetProps,
494
+ scaleFeedbackTriggerProps,
495
+ nativeProps,
496
+ )}
471
497
  flatten={false}
472
- bindlayoutchange={handleLayoutChange}
473
498
  accessibility-element={true}
474
499
  accessibility-role-description="tab"
475
500
  accessibility-label={accessibilityLabel ?? label}
@@ -478,7 +503,14 @@ export const TabsTrigger = React.forwardRef<unknown, TabsTriggerProps>((props, r
478
503
  className={clsx(triggerClasses.trigger, className)}
479
504
  style={style}
480
505
  >
481
- <text className={triggerClasses.triggerLabel}>{children}</text>
506
+ {notification ? (
507
+ <Box position="relative">
508
+ <text className={triggerClasses.triggerLabel}>{children}</text>
509
+ {notification}
510
+ </Box>
511
+ ) : (
512
+ <text className={triggerClasses.triggerLabel}>{children}</text>
513
+ )}
482
514
  </view>
483
515
  );
484
516
  });
@@ -507,9 +539,11 @@ export const TabsIndicator = React.forwardRef<unknown, TabsIndicatorProps>((prop
507
539
 
508
540
  return (
509
541
  <view
510
- {...(ref ? { ref: ref as LynxViewRef } : {})}
511
- main-thread:ref={indicatorRef}
512
- {...nativeProps}
542
+ {...mergeProps(
543
+ { "main-thread:ref": indicatorRef },
544
+ ref ? { ref: ref as LynxViewRef } : {},
545
+ nativeProps,
546
+ )}
513
547
  accessibility-elements-hidden={true}
514
548
  className={clsx(classNames.indicator, className)}
515
549
  style={
@@ -545,8 +579,7 @@ export const TabsContent = React.forwardRef<unknown, TabsContentProps>((props, r
545
579
 
546
580
  const content = (
547
581
  <view
548
- {...(ref ? { ref: ref as LynxViewRef } : {})}
549
- {...nativeProps}
582
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
550
583
  accessibility-elements-hidden={!selected}
551
584
  accessibility-role-description="tabpanel"
552
585
  accessibility-value={selected ? "selected" : "not selected"}
@@ -603,8 +636,7 @@ export const TabsCarousel = React.forwardRef<unknown, TabsCarouselProps>((props,
603
636
  return (
604
637
  <TabsCarouselContext.Provider value={contextValue}>
605
638
  <view
606
- {...(ref ? { ref: ref as LynxViewRef } : {})}
607
- {...nativeProps}
639
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
608
640
  className={clsx(classNames.carousel, className)}
609
641
  style={style}
610
642
  >
@@ -641,12 +673,8 @@ export const TabsCarouselCamera = React.forwardRef<unknown, TabsCarouselCameraPr
641
673
  const { indicatorRef, pagerValues, triggerRects } = tabsContext;
642
674
  const indicatorRects = pagerValues.map((value) => triggerRects[value] ?? null);
643
675
 
644
- const mergedRef = React.useCallback(
645
- (node: NodesRef | null) => {
646
- tabsContext.setPagerRef(node);
647
- if (typeof ref === "function") ref(node);
648
- else if (ref) ref.current = node;
649
- },
676
+ const mergedRef = React.useMemo(
677
+ () => mergeProps({ ref: tabsContext.setPagerRef }, { ref: ref as LynxViewRef }).ref,
650
678
  [ref, tabsContext.setPagerRef],
651
679
  );
652
680
 
@@ -717,15 +745,19 @@ export const TabsCarouselCamera = React.forwardRef<unknown, TabsCarouselCameraPr
717
745
 
718
746
  return (
719
747
  <viewpager
720
- ref={mergedRef}
721
- {...nativeProps}
748
+ {...mergeProps(
749
+ {
750
+ ref: mergedRef,
751
+ bindwillchange: handleWillChange,
752
+ bindchange: handleChange,
753
+ bindoffsetchange: handleOffsetChange,
754
+ "main-thread:bindoffsetchange": handleIndicatorOffsetChange,
755
+ },
756
+ nativeProps,
757
+ )}
722
758
  initial-select-index={Math.max(0, tabsContext.selectedPagerIndex)}
723
759
  enable-scroll={carouselContext.swipeable}
724
760
  ios-gesture-offset={carouselContext.iosBackGestureEdgeWidth}
725
- bindwillchange={handleWillChange}
726
- bindchange={handleChange}
727
- bindoffsetchange={handleOffsetChange}
728
- main-thread:bindoffsetchange={handleIndicatorOffsetChange}
729
761
  className={clsx(classNames.carouselCamera, className)}
730
762
  style={style}
731
763
  >
@@ -11,6 +11,7 @@ import { toArray } from "../../utils/children";
11
11
  import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
12
12
  import { splitMultipleVariantsProps } from "../../utils/split-multiple-variants-props";
13
13
  import { useStyleProps, type StyleProps } from "../../utils/styled";
14
+ import { mergeProps } from "../../utils/merge-props";
14
15
 
15
16
  const { PropsProvider, useProps, ClassNamesProvider, useClassNames } =
16
17
  createSlotRecipeContext(tagGroupItem);
@@ -49,9 +50,8 @@ export const TagGroupRoot = React.forwardRef<unknown, TagGroupRootProps>((props,
49
50
  return (
50
51
  <PropsProvider value={tagGroupItemVariantProps}>
51
52
  <view
52
- {...(ref ? { ref: ref as LynxViewRef } : {})}
53
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
53
54
  className={clsx(classes.root, className)}
54
- {...nativeProps}
55
55
  >
56
56
  {visibleChildren.map((child, index) => {
57
57
  if (index === 0) return child;
@@ -97,10 +97,9 @@ export const TagGroupItem = React.forwardRef<unknown, TagGroupItemProps>((props,
97
97
  return (
98
98
  <ClassNamesProvider value={classes}>
99
99
  <view
100
- {...(ref ? { ref: ref as LynxViewRef } : {})}
100
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
101
101
  className={clsx(classes.root, className)}
102
102
  style={style}
103
- {...nativeProps}
104
103
  >
105
104
  {children}
106
105
  </view>
@@ -118,9 +117,8 @@ export const TagGroupItemLabel = React.forwardRef<unknown, TagGroupItemLabelProp
118
117
  const { children, className, ...nativeProps } = props;
119
118
  return (
120
119
  <text
121
- {...(ref ? { ref: ref as LynxTextRef } : {})}
120
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
122
121
  className={clsx(classes.label, className)}
123
- {...nativeProps}
124
122
  >
125
123
  {children}
126
124
  </text>
@@ -12,6 +12,7 @@ import {
12
12
  type TextStyleProps,
13
13
  } from "../../utils/styled";
14
14
  import type { LynxStyledElementProps, LynxTextRef } from "../../types";
15
+ import { mergeProps } from "../../utils/merge-props";
15
16
 
16
17
  function capitalize<T extends string>(value: T): Capitalize<T> {
17
18
  return (value.charAt(0).toUpperCase() + value.slice(1)) as Capitalize<T>;
@@ -42,8 +43,7 @@ export const Text = React.forwardRef<unknown, TextProps>((props, ref) => {
42
43
 
43
44
  return (
44
45
  <text
45
- {...(ref ? { ref: ref as LynxTextRef } : {})}
46
- {...nativeProps}
46
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
47
47
  className={clsx(className)}
48
48
  style={
49
49
  {