@seed-design/lynx-react 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/lib/components/Accordion/Accordion.jsx +2 -2
  2. package/lib/components/ActionButton/ActionButton.jsx +1 -1
  3. package/lib/components/AlertDialog/AlertDialog.d.ts +64 -0
  4. package/lib/components/AlertDialog/AlertDialog.d.ts.map +1 -0
  5. package/lib/components/AlertDialog/AlertDialog.jsx +138 -0
  6. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts +2 -0
  7. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts.map +1 -0
  8. package/lib/components/AlertDialog/AlertDialog.namespace.js +17 -0
  9. package/lib/components/AlertDialog/index.d.ts +3 -0
  10. package/lib/components/AlertDialog/index.d.ts.map +1 -0
  11. package/lib/components/AppBar/AppBar.d.ts +14 -1
  12. package/lib/components/AppBar/AppBar.d.ts.map +1 -1
  13. package/lib/components/AppBar/AppBar.jsx +46 -10
  14. package/lib/components/AppBar/AppBar.namespace.d.ts +1 -1
  15. package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -1
  16. package/lib/components/AppBar/index.d.ts +1 -1
  17. package/lib/components/AppBar/index.d.ts.map +1 -1
  18. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts +182 -0
  19. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts.map +1 -0
  20. package/lib/components/AttachmentDisplay/AttachmentDisplay.jsx +444 -0
  21. package/lib/components/AttachmentDisplay/index.d.ts +2 -0
  22. package/lib/components/AttachmentDisplay/index.d.ts.map +1 -0
  23. package/lib/components/AttachmentInput/AttachmentInput.d.ts +179 -0
  24. package/lib/components/AttachmentInput/AttachmentInput.d.ts.map +1 -0
  25. package/lib/components/AttachmentInput/AttachmentInput.jsx +520 -0
  26. package/lib/components/AttachmentInput/index.d.ts +2 -0
  27. package/lib/components/AttachmentInput/index.d.ts.map +1 -0
  28. package/lib/components/BottomSheet/BottomSheet.d.ts +1 -1
  29. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  30. package/lib/components/BottomSheet/BottomSheet.jsx +1 -7
  31. package/lib/components/Callout/Callout.jsx +2 -2
  32. package/lib/components/Checkbox/Checkbox.jsx +2 -2
  33. package/lib/components/Chip/Chip.jsx +2 -2
  34. package/lib/components/ChipTabs/ChipTabs.d.ts +41 -0
  35. package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -0
  36. package/lib/components/ChipTabs/ChipTabs.jsx +39 -0
  37. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts +2 -0
  38. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts.map +1 -0
  39. package/lib/components/ChipTabs/ChipTabs.namespace.js +13 -0
  40. package/lib/components/ChipTabs/index.d.ts +3 -0
  41. package/lib/components/ChipTabs/index.d.ts.map +1 -0
  42. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts +18 -0
  43. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -0
  44. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.jsx +113 -0
  45. package/lib/components/ContextualFloatingButton/index.d.ts +2 -0
  46. package/lib/components/ContextualFloatingButton/index.d.ts.map +1 -0
  47. package/lib/components/Dialog/Dialog.d.ts +87 -0
  48. package/lib/components/Dialog/Dialog.d.ts.map +1 -0
  49. package/lib/components/Dialog/Dialog.jsx +147 -0
  50. package/lib/components/Dialog/Dialog.namespace.d.ts +2 -0
  51. package/lib/components/Dialog/Dialog.namespace.d.ts.map +1 -0
  52. package/lib/components/Dialog/Dialog.namespace.js +18 -0
  53. package/lib/components/Dialog/index.d.ts +3 -0
  54. package/lib/components/Dialog/index.d.ts.map +1 -0
  55. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts +23 -0
  56. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -0
  57. package/lib/components/FloatingActionButton/FloatingActionButton.jsx +76 -0
  58. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts +2 -0
  59. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts.map +1 -0
  60. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.js +10 -0
  61. package/lib/components/FloatingActionButton/index.d.ts +3 -0
  62. package/lib/components/FloatingActionButton/index.d.ts.map +1 -0
  63. package/lib/components/HelpBubble/HelpBubble.d.ts +67 -0
  64. package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -0
  65. package/lib/components/HelpBubble/HelpBubble.jsx +600 -0
  66. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts +2 -0
  67. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts.map +1 -0
  68. package/lib/components/HelpBubble/HelpBubble.namespace.js +18 -0
  69. package/lib/components/HelpBubble/index.d.ts +3 -0
  70. package/lib/components/HelpBubble/index.d.ts.map +1 -0
  71. package/lib/components/Icon/Icon.d.ts +1 -0
  72. package/lib/components/Icon/Icon.d.ts.map +1 -1
  73. package/lib/components/Icon/Icon.jsx +3 -2
  74. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts +28 -0
  75. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts.map +1 -0
  76. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.jsx +37 -0
  77. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts +2 -0
  78. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts.map +1 -0
  79. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.js +9 -0
  80. package/lib/components/IdentityPlaceholder/identity-placeholder-business.js +4 -0
  81. package/lib/components/IdentityPlaceholder/identity-placeholder-person.js +4 -0
  82. package/lib/components/IdentityPlaceholder/index.d.ts +3 -0
  83. package/lib/components/IdentityPlaceholder/index.d.ts.map +1 -0
  84. package/lib/components/InputButton/InputButton.d.ts +2 -0
  85. package/lib/components/InputButton/InputButton.d.ts.map +1 -1
  86. package/lib/components/InputButton/InputButton.jsx +40 -8
  87. package/lib/components/List/List.d.ts +1 -1
  88. package/lib/components/List/List.d.ts.map +1 -1
  89. package/lib/components/List/List.jsx +26 -12
  90. package/lib/components/Menu/Menu.d.ts.map +1 -1
  91. package/lib/components/Menu/Menu.jsx +15 -5
  92. package/lib/components/PageBanner/PageBanner.d.ts +3 -0
  93. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
  94. package/lib/components/PageBanner/PageBanner.jsx +49 -11
  95. package/lib/components/QuantityPicker/QuantityPicker.d.ts +53 -0
  96. package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -0
  97. package/lib/components/QuantityPicker/QuantityPicker.jsx +282 -0
  98. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts +2 -0
  99. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts.map +1 -0
  100. package/lib/components/QuantityPicker/QuantityPicker.namespace.js +11 -0
  101. package/lib/components/QuantityPicker/index.d.ts +3 -0
  102. package/lib/components/QuantityPicker/index.d.ts.map +1 -0
  103. package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
  104. package/lib/components/ReactionButton/ReactionButton.d.ts +1 -1
  105. package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
  106. package/lib/components/ReactionButton/ReactionButton.jsx +22 -13
  107. package/lib/components/ScrollFog/ScrollFog.d.ts +41 -0
  108. package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -0
  109. package/lib/components/ScrollFog/ScrollFog.jsx +65 -0
  110. package/lib/components/ScrollFog/index.d.ts +2 -0
  111. package/lib/components/ScrollFog/index.d.ts.map +1 -0
  112. package/lib/components/SegmentedControl/SegmentedControl.jsx +2 -2
  113. package/lib/components/Select/Select.d.ts +112 -0
  114. package/lib/components/Select/Select.d.ts.map +1 -0
  115. package/lib/components/Select/Select.jsx +885 -0
  116. package/lib/components/Select/Select.namespace.d.ts +2 -0
  117. package/lib/components/Select/Select.namespace.d.ts.map +1 -0
  118. package/lib/components/Select/Select.namespace.js +23 -0
  119. package/lib/components/Select/index.d.ts +3 -0
  120. package/lib/components/Select/index.d.ts.map +1 -0
  121. package/lib/components/Skeleton/Skeleton.d.ts +15 -0
  122. package/lib/components/Skeleton/Skeleton.d.ts.map +1 -0
  123. package/lib/components/Skeleton/Skeleton.jsx +18 -0
  124. package/lib/components/Skeleton/index.d.ts +2 -0
  125. package/lib/components/Skeleton/index.d.ts.map +1 -0
  126. package/lib/components/Slider/Slider.d.ts +101 -0
  127. package/lib/components/Slider/Slider.d.ts.map +1 -0
  128. package/lib/components/Slider/Slider.jsx +573 -0
  129. package/lib/components/Slider/Slider.namespace.d.ts +2 -0
  130. package/lib/components/Slider/Slider.namespace.d.ts.map +1 -0
  131. package/lib/components/Slider/Slider.namespace.js +19 -0
  132. package/lib/components/Slider/Slider.utils.d.ts +19 -0
  133. package/lib/components/Slider/Slider.utils.d.ts.map +1 -0
  134. package/lib/components/Slider/Slider.utils.js +106 -0
  135. package/lib/components/Slider/index.d.ts +3 -0
  136. package/lib/components/Slider/index.d.ts.map +1 -0
  137. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
  138. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +22 -8
  139. package/lib/components/Switch/Switch.d.ts.map +1 -1
  140. package/lib/components/Switch/Switch.jsx +5 -3
  141. package/lib/components/Tabs/Tabs.d.ts +34 -2
  142. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  143. package/lib/components/Tabs/Tabs.jsx +186 -56
  144. package/lib/components/Tabs/Tabs.utils.d.ts +14 -1
  145. package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
  146. package/lib/components/Tabs/Tabs.utils.js +24 -3
  147. package/lib/components/Tabs/index.d.ts +1 -1
  148. package/lib/components/Tabs/index.d.ts.map +1 -1
  149. package/lib/components/index.d.ts +14 -0
  150. package/lib/components/index.d.ts.map +1 -1
  151. package/lib/hooks/useControllableState.d.ts +1 -14
  152. package/lib/hooks/useControllableState.d.ts.map +1 -1
  153. package/lib/hooks/usePressTap.d.ts +1 -27
  154. package/lib/hooks/usePressTap.d.ts.map +1 -1
  155. package/lib/hooks/useScaleFeedback.d.ts +1 -1
  156. package/lib/hooks/useScaleFeedback.js +1 -1
  157. package/lib/index.js +29 -6
  158. package/lib/{hooks/useControllableState.js → packages/lynx-react-headless/use-controllable-state/lib/index.js} +2 -3
  159. package/lib/{hooks/usePressTap.js → packages/lynx-react-headless/use-press-tap/lib/index.js} +13 -10
  160. package/lib/types.d.ts +1 -0
  161. package/lib/types.d.ts.map +1 -1
  162. package/package.json +9 -5
  163. package/src/components/ActionButton/ActionButton.test.tsx +7 -7
  164. package/src/components/AlertDialog/AlertDialog.namespace.ts +22 -0
  165. package/src/components/AlertDialog/AlertDialog.test.tsx +226 -0
  166. package/src/components/AlertDialog/AlertDialog.tsx +374 -0
  167. package/src/components/AlertDialog/index.ts +24 -0
  168. package/src/components/AppBar/AppBar.namespace.ts +1 -0
  169. package/src/components/AppBar/AppBar.test.tsx +257 -0
  170. package/src/components/AppBar/AppBar.tsx +109 -14
  171. package/src/components/AppBar/index.ts +1 -0
  172. package/src/components/AttachmentDisplay/AttachmentDisplay.tsx +886 -0
  173. package/src/components/AttachmentDisplay/index.ts +48 -0
  174. package/src/components/AttachmentInput/AttachmentInput.tsx +1001 -0
  175. package/src/components/AttachmentInput/index.ts +49 -0
  176. package/src/components/BottomSheet/BottomSheet.test.tsx +22 -7
  177. package/src/components/BottomSheet/BottomSheet.tsx +1 -19
  178. package/src/components/ChipTabs/ChipTabs.namespace.ts +14 -0
  179. package/src/components/ChipTabs/ChipTabs.tsx +119 -0
  180. package/src/components/ChipTabs/index.ts +16 -0
  181. package/src/components/ContextualFloatingButton/ContextualFloatingButton.test.tsx +63 -0
  182. package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +232 -0
  183. package/src/components/ContextualFloatingButton/index.ts +4 -0
  184. package/src/components/Dialog/Dialog.namespace.ts +24 -0
  185. package/src/components/Dialog/Dialog.test.tsx +172 -0
  186. package/src/components/Dialog/Dialog.tsx +373 -0
  187. package/src/components/Dialog/index.ts +26 -0
  188. package/src/components/FloatingActionButton/FloatingActionButton.namespace.ts +8 -0
  189. package/src/components/FloatingActionButton/FloatingActionButton.test.tsx +59 -0
  190. package/src/components/FloatingActionButton/FloatingActionButton.tsx +207 -0
  191. package/src/components/FloatingActionButton/index.ts +10 -0
  192. package/src/components/HelpBubble/HelpBubble.namespace.ts +24 -0
  193. package/src/components/HelpBubble/HelpBubble.tsx +998 -0
  194. package/src/components/HelpBubble/index.ts +26 -0
  195. package/src/components/Icon/Icon.test.tsx +57 -5
  196. package/src/components/Icon/Icon.tsx +16 -2
  197. package/src/components/IdentityPlaceholder/IdentityPlaceholder.namespace.ts +6 -0
  198. package/src/components/IdentityPlaceholder/IdentityPlaceholder.tsx +103 -0
  199. package/src/components/IdentityPlaceholder/assets.d.ts +4 -0
  200. package/src/components/IdentityPlaceholder/identity-placeholder-business.webp +0 -0
  201. package/src/components/IdentityPlaceholder/identity-placeholder-person.webp +0 -0
  202. package/src/components/IdentityPlaceholder/index.ts +8 -0
  203. package/src/components/InputButton/InputButton.test.tsx +128 -1
  204. package/src/components/InputButton/InputButton.tsx +60 -9
  205. package/src/components/List/List.test.tsx +54 -2
  206. package/src/components/List/List.tsx +43 -10
  207. package/src/components/Menu/Menu.tsx +14 -4
  208. package/src/components/Menu/__tests__/Menu.test.tsx +34 -0
  209. package/src/components/PageBanner/PageBanner.test.tsx +110 -3
  210. package/src/components/PageBanner/PageBanner.tsx +83 -19
  211. package/src/components/QuantityPicker/QuantityPicker.namespace.ts +10 -0
  212. package/src/components/QuantityPicker/QuantityPicker.test.tsx +235 -0
  213. package/src/components/QuantityPicker/QuantityPicker.tsx +605 -0
  214. package/src/components/QuantityPicker/index.ts +14 -0
  215. package/src/components/ReactionButton/ReactionButton.feedback.test.tsx +50 -0
  216. package/src/components/ReactionButton/ReactionButton.test.tsx +6 -4
  217. package/src/components/ReactionButton/ReactionButton.tsx +20 -12
  218. package/src/components/ScrollFog/ScrollFog.test.tsx +222 -0
  219. package/src/components/ScrollFog/ScrollFog.tsx +172 -0
  220. package/src/components/ScrollFog/index.ts +1 -0
  221. package/src/components/Select/Select.namespace.ts +34 -0
  222. package/src/components/Select/Select.test.tsx +297 -0
  223. package/src/components/Select/Select.tsx +1483 -0
  224. package/src/components/Select/index.ts +39 -0
  225. package/src/components/Skeleton/Skeleton.test.tsx +64 -0
  226. package/src/components/Skeleton/Skeleton.tsx +45 -0
  227. package/src/components/Skeleton/index.ts +1 -0
  228. package/src/components/Slider/Slider.namespace.ts +26 -0
  229. package/src/components/Slider/Slider.test.tsx +260 -0
  230. package/src/components/Slider/Slider.tsx +897 -0
  231. package/src/components/Slider/Slider.utils.ts +167 -0
  232. package/src/components/Slider/index.ts +30 -0
  233. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +21 -1
  234. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +30 -7
  235. package/src/components/Switch/Switch.tsx +14 -13
  236. package/src/components/Tabs/Tabs.test.tsx +218 -1
  237. package/src/components/Tabs/Tabs.tsx +346 -75
  238. package/src/components/Tabs/Tabs.utils.ts +51 -1
  239. package/src/components/Tabs/index.ts +17 -1
  240. package/src/components/index.ts +14 -0
  241. package/src/hooks/useControllableState.ts +1 -43
  242. package/src/hooks/usePressTap.test.ts +23 -11
  243. package/src/hooks/usePressTap.ts +1 -60
  244. package/src/hooks/useScaleFeedback.ts +1 -1
  245. package/src/types.ts +1 -0
@@ -0,0 +1,26 @@
1
+ export {
2
+ HelpBubbleAnchor,
3
+ HelpBubbleArrow,
4
+ HelpBubbleArrowTip,
5
+ HelpBubbleBody,
6
+ HelpBubbleCloseButton,
7
+ HelpBubbleContent,
8
+ HelpBubbleDescription,
9
+ HelpBubblePositioner,
10
+ HelpBubbleRoot,
11
+ HelpBubbleTitle,
12
+ HelpBubbleTrigger,
13
+ type HelpBubbleAnchorProps,
14
+ type HelpBubbleArrowProps,
15
+ type HelpBubbleArrowTipProps,
16
+ type HelpBubbleBodyProps,
17
+ type HelpBubbleCloseButtonProps,
18
+ type HelpBubbleContentProps,
19
+ type HelpBubbleDescriptionProps,
20
+ type HelpBubblePositionerProps,
21
+ type HelpBubbleRootProps,
22
+ type HelpBubbleTitleProps,
23
+ type HelpBubbleTriggerProps,
24
+ } from "./HelpBubble";
25
+
26
+ export * as HelpBubble from "./HelpBubble.namespace";
@@ -1,10 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+
1
4
  import * as React from "@lynx-js/react";
2
5
  import { act, render, waitSchedule } from "@lynx-js/react/testing-library";
3
6
  import type { MainThread } from "@lynx-js/types";
4
7
  import { describe, expect, it } from "vitest";
5
8
 
6
9
  import type { LynxIconElementProps } from "../../types";
7
- import { InternalIcon, PrefixIcon } from "./Icon";
10
+ import { Icon, IconSlotProvider, InternalIcon, PrefixIcon, SuffixIcon } from "./Icon";
8
11
 
9
12
  const TestIcon = React.forwardRef<
10
13
  MainThread.Element,
@@ -94,15 +97,13 @@ describe("InternalIcon", () => {
94
97
  await waitSchedule();
95
98
 
96
99
  const { image } = getSourceAndImage();
97
- expect(frames.size).toBe(0);
98
- expect(image.getAttribute("tint-color")).toBeNull();
99
100
 
100
101
  rerender(renderIcon("checked"));
101
102
  await waitSchedule();
102
103
 
103
104
  expect(frames.size).toBe(1);
104
- expect(image.getAttribute("tint-color")).toBeNull();
105
-
105
+ // The immediate read uses the old native style; the next frame must refresh it.
106
+ expect(image.getAttribute("tint-color")).toBe("rgb(134, 139, 148)");
106
107
  computedColor = "rgb(255, 102, 0)";
107
108
  runNextFrame();
108
109
 
@@ -149,3 +150,54 @@ describe("PrefixIcon", () => {
149
150
  expect(getSourceAndImage().image.getAttribute("tint-color")).toBeNull();
150
151
  });
151
152
  });
153
+
154
+ describe("icon recipe ownership", () => {
155
+ it.each([
156
+ [Icon, "icon", "seed-icon"],
157
+ [PrefixIcon, "prefixIcon", "seed-prefix-icon"],
158
+ [SuffixIcon, "suffixIcon", "seed-suffix-icon"],
159
+ ] as const)("keeps fallback styles exclusive to unbound %s", (Component, slot, baseClass) => {
160
+ const { container, rerender } = render(<Component icon={<TestIcon />} />);
161
+ expect(container.querySelector(`.${baseClass}`)).not.toBeNull();
162
+ rerender(
163
+ <IconSlotProvider value={{ classNames: { [slot]: "test-recipe-icon" }, deps: [] }}>
164
+ <Component size={28} color="#2475e8" className="user-icon" icon={<TestIcon />} />
165
+ </IconSlotProvider>,
166
+ );
167
+ const wrapper = container.querySelector(".test-recipe-icon")!;
168
+ expect(wrapper.classList.contains(`${baseClass}-slot`)).toBe(true);
169
+ expect(wrapper.classList.contains("user-icon")).toBe(true);
170
+ expect(wrapper.classList.contains(baseClass)).toBe(false);
171
+ expect(wrapper.getAttribute("style")).toContain("28px");
172
+ });
173
+ });
174
+
175
+ // This checks generated CSS cascade only; native state/theme invalidation is verified separately.
176
+ describe("generated icon CSS import order", () => {
177
+ const { JSDOM } = createRequire(`${process.cwd()}/package.json`)("jsdom") as {
178
+ JSDOM: new (html: string) => { window: Window & typeof globalThis };
179
+ };
180
+ const base = readFileSync("../lynx-css/base.css", "utf8");
181
+ const recipe = readFileSync("../lynx-css/recipes/callout.css", "utf8");
182
+
183
+ it("keeps the legacy standalone fallback selectors", () => {
184
+ const dom = new JSDOM(`<style>${base}</style><div class="seed-suffix-icon"></div>`);
185
+ const style = dom.window.getComputedStyle(dom.window.document.querySelector("div")!);
186
+ expect(style.width).toBe("var(--seed-suffix-icon-size)");
187
+ expect(style.height).toBe("var(--seed-suffix-icon-size)");
188
+ expect(style.color).toBe("var(--seed-suffix-icon-color, currentcolor)");
189
+ dom.window.close();
190
+ });
191
+
192
+ it.each(["base-first", "recipe-first"])("preserves Callout slot styles with %s", (order) => {
193
+ const dom = new JSDOM(`<style>${order === "base-first" ? base + recipe : recipe + base}</style>
194
+ <div class="seed-suffix-icon-slot seed-callout__suffixIcon seed-callout__suffixIcon--tone_neutral"></div>`);
195
+ const icon = dom.window.document.querySelector("div")!;
196
+ const style = dom.window.getComputedStyle(icon);
197
+ expect(style.width).toBe("var(--seed-dimension-x4)");
198
+ expect(style.height).toBe("var(--seed-dimension-x4)");
199
+ expect(style.color).toBe("var(--seed-color-fg-neutral)");
200
+ expect(style.flexShrink).toBe("0");
201
+ dom.window.close();
202
+ });
203
+ });
@@ -205,7 +205,11 @@ const IconSlotBase = React.forwardRef<unknown, IconSlotBaseProps>((props, ref) =
205
205
  ref ? { ref: ref as LynxViewRef } : {},
206
206
  nativeProps,
207
207
  )}
208
- className={clsx(baseClassName, slotClassName, className)}
208
+ className={clsx(
209
+ slotClassName && baseClassName ? `${baseClassName}-slot` : baseClassName,
210
+ slotClassName,
211
+ className,
212
+ )}
209
213
  style={mergeWrapperStyle({ size, color, style })}
210
214
  >
211
215
  {cloneElement(icon, {
@@ -258,10 +262,19 @@ export const SuffixIcon = createIconComponent<SuffixIconProps>(
258
262
  export interface InternalIconProps extends LynxStyledElementProps {
259
263
  icon: ReactElement<LynxIconElementProps>;
260
264
  deps?: DependencyList;
265
+ disableDefaultResize?: boolean;
261
266
  }
262
267
 
263
268
  export const InternalIcon = React.forwardRef<unknown, InternalIconProps>((props, ref) => {
264
- const { icon, deps = [], className, style, children: _children, ...nativeProps } = props;
269
+ const {
270
+ icon,
271
+ deps = [],
272
+ className,
273
+ style,
274
+ children: _children,
275
+ disableDefaultResize = false,
276
+ ...nativeProps
277
+ } = props;
265
278
  const sourceRef = useMainThreadRef<MainThread.Element>(null);
266
279
  const styleColor = getStyleColor(style);
267
280
  const iconColor = useIconColor([className, styleColor, ...(deps ?? [])], { sourceRef });
@@ -280,6 +293,7 @@ export const InternalIcon = React.forwardRef<unknown, InternalIconProps>((props,
280
293
  >
281
294
  {cloneElement(icon, {
282
295
  ...iconColor,
296
+ ...(disableDefaultResize ? { "disable-default-resize": true } : {}),
283
297
  className: icon.props.className,
284
298
  style: {
285
299
  ...icon.props.style,
@@ -0,0 +1,6 @@
1
+ export {
2
+ IdentityPlaceholderRoot as Root,
3
+ IdentityPlaceholderImage as Image,
4
+ type IdentityPlaceholderRootProps as RootProps,
5
+ type IdentityPlaceholderImageProps as ImageProps,
6
+ } from "./IdentityPlaceholder";
@@ -0,0 +1,103 @@
1
+ import {
2
+ identityPlaceholder,
3
+ type IdentityPlaceholderVariantProps,
4
+ } from "@seed-design/lynx-css/recipes/identity-placeholder";
5
+ import type { IntrinsicElements, NodesRef } from "@lynx-js/types";
6
+ import clsx from "clsx";
7
+ import * as React from "@lynx-js/react";
8
+
9
+ import type { LynxStyledElementProps, LynxViewRef } from "../../types";
10
+ import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
11
+ import { mergeProps } from "../../utils/merge-props";
12
+ import businessSource from "./identity-placeholder-business.webp";
13
+ import personSource from "./identity-placeholder-person.webp";
14
+
15
+ const { ClassNamesProvider, PropsProvider, useClassNames, useProps } =
16
+ createSlotRecipeContext(identityPlaceholder);
17
+
18
+ type Identity = NonNullable<IdentityPlaceholderVariantProps["identity"]>;
19
+
20
+ const identitySources = {
21
+ person: personSource,
22
+ business: businessSource,
23
+ } satisfies Record<Identity, string>;
24
+
25
+ /**
26
+ * @platform Lynx
27
+ *
28
+ * Web의 SVG/asChild API 대신 native `<view>` props를 받습니다.
29
+ */
30
+ export interface IdentityPlaceholderRootProps
31
+ extends IdentityPlaceholderVariantProps,
32
+ LynxStyledElementProps {}
33
+
34
+ export const IdentityPlaceholderRoot = React.forwardRef<unknown, IdentityPlaceholderRootProps>(
35
+ (props, ref) => {
36
+ const [variantProps, otherProps] = identityPlaceholder.splitVariantProps(props);
37
+ const { children, className, style, ...nativeProps } = otherProps;
38
+ const classNames = identityPlaceholder(variantProps);
39
+
40
+ return (
41
+ <ClassNamesProvider value={classNames}>
42
+ <PropsProvider value={variantProps}>
43
+ <view
44
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
45
+ className={clsx(classNames.root, className)}
46
+ style={style}
47
+ >
48
+ {children}
49
+ </view>
50
+ </PropsProvider>
51
+ </ClassNamesProvider>
52
+ );
53
+ },
54
+ );
55
+ IdentityPlaceholderRoot.displayName = "IdentityPlaceholderRoot";
56
+
57
+ /**
58
+ * @platform Lynx
59
+ *
60
+ * `src`와 `mode`는 identity variant가 소유합니다. Web SVG props 대신 native image
61
+ * props를 사용하며 SVG rendering과 `asChild`는 지원하지 않습니다.
62
+ *
63
+ * React SVG의 기본 `xMidYMid meet`처럼 도형 전체를 비율을 유지한 채 영역 안에 맞춥니다.
64
+ *
65
+ * 로컬 WebP는 `static-white-alpha-800`의 고정값 `#ffffffde`로 원본 SVG path를
66
+ * rasterize합니다. Native image의 SVG/fill 경로를 피하고, 토큰이 light/dark에서
67
+ * 동일하므로 동적 tint를 사용하지 않습니다.
68
+ */
69
+ export interface IdentityPlaceholderImageProps
70
+ extends Omit<IntrinsicElements["image"], "src" | "mode" | "children"> {}
71
+
72
+ export const IdentityPlaceholderImage = React.forwardRef<unknown, IdentityPlaceholderImageProps>(
73
+ (props, ref) => {
74
+ const classNames = useClassNames();
75
+ const parentProps = useProps();
76
+
77
+ if (parentProps === null) {
78
+ throw new Error(
79
+ "<IdentityPlaceholderImage/> must be rendered inside <IdentityPlaceholderRoot/>.",
80
+ );
81
+ }
82
+
83
+ const {
84
+ className,
85
+ "accessibility-label": accessibilityLabel,
86
+ "accessibility-traits": accessibilityTraits,
87
+ ...nativeProps
88
+ } = props;
89
+ const identity = parentProps.identity ?? "person";
90
+
91
+ return (
92
+ <image
93
+ {...mergeProps(ref ? { ref: ref as React.Ref<NodesRef> } : {}, nativeProps)}
94
+ src={identitySources[identity]}
95
+ mode="aspectFit"
96
+ accessibility-label={accessibilityLabel ?? "Identity placeholder"}
97
+ accessibility-traits={accessibilityTraits ?? "image"}
98
+ className={clsx(classNames.image, className)}
99
+ />
100
+ );
101
+ },
102
+ );
103
+ IdentityPlaceholderImage.displayName = "IdentityPlaceholderImage";
@@ -0,0 +1,4 @@
1
+ declare module "*.webp" {
2
+ const source: string;
3
+ export default source;
4
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ IdentityPlaceholderRoot,
3
+ IdentityPlaceholderImage,
4
+ type IdentityPlaceholderRootProps,
5
+ type IdentityPlaceholderImageProps,
6
+ } from "./IdentityPlaceholder";
7
+
8
+ export * as IdentityPlaceholder from "./IdentityPlaceholder.namespace";
@@ -1,8 +1,22 @@
1
1
  import "@testing-library/jest-dom";
2
- import { fireEvent, getQueriesForElement, render } from "@lynx-js/react/testing-library";
2
+ import * as React from "@lynx-js/react";
3
+ import { runOnBackground } from "@lynx-js/react";
4
+ import {
5
+ fireEvent,
6
+ getQueriesForElement,
7
+ render,
8
+ waitSchedule,
9
+ } from "@lynx-js/react/testing-library";
3
10
  import { describe, expect, it, vi } from "vitest";
4
11
 
5
12
  import { InputButton } from "./index";
13
+ import type { LynxIconElementProps } from "../../types";
14
+ import type { MainThread } from "@lynx-js/types";
15
+
16
+ const MockIcon = React.forwardRef<MainThread.Element, LynxIconElementProps>((props, ref) => (
17
+ <image {...props} {...(ref ? { "main-thread:ref": ref as React.Ref<MainThread.Element> } : {})} />
18
+ ));
19
+ MockIcon.displayName = "MockIcon";
6
20
 
7
21
  function getRenderedRoot() {
8
22
  const root = elementTree.root;
@@ -76,6 +90,119 @@ describe("InputButton", () => {
76
90
  );
77
91
  });
78
92
 
93
+ it("groups fragment content while leaving the button and strokes outside the scale target", () => {
94
+ render(
95
+ <InputButton.Root>
96
+ <React.Fragment key="fragment-slots">
97
+ <InputButton.Button accessibility-label="Select" />
98
+ <InputButton.PrefixText>Prefix</InputButton.PrefixText>
99
+ <InputButton.Value>Value</InputButton.Value>
100
+ <InputButton.SuffixText>Suffix</InputButton.SuffixText>
101
+ </React.Fragment>
102
+ </InputButton.Root>,
103
+ );
104
+
105
+ const root = getRenderedRoot();
106
+ const content = root.querySelector(".seed-input-button__content");
107
+ const button = root.querySelector(".seed-input-button__button");
108
+ expect(content).toHaveAttribute("flatten", "false");
109
+ expect(content?.contains(button)).toBe(false);
110
+ expect(button?.querySelector(".seed-input-button__baseStroke")).toBeTruthy();
111
+ expect(button?.querySelector(".seed-input-button__stroke")).toBeTruthy();
112
+ expect(Array.from(content?.children ?? []).map((child) => child.textContent)).toEqual([
113
+ "Prefix",
114
+ "Value",
115
+ "Suffix",
116
+ ]);
117
+ });
118
+
119
+ it("preserves custom wrapped Button composition without moving or dropping children", () => {
120
+ const CustomButton = () => <InputButton.Button accessibility-label="Custom" />;
121
+ render(
122
+ <InputButton.Root>
123
+ <CustomButton />
124
+ <InputButton.Value>Value</InputButton.Value>
125
+ </InputButton.Root>,
126
+ );
127
+
128
+ const root = getRenderedRoot();
129
+ expect(root.querySelector(".seed-input-button__button")).toHaveAttribute(
130
+ "accessibility-label",
131
+ "Custom",
132
+ );
133
+ expect(root.querySelector(".seed-input-button__content")).toBeNull();
134
+ });
135
+
136
+ it("keeps the clear action independent and hides it in readonly mode", () => {
137
+ const onOpen = vi.fn();
138
+ const onClear = vi.fn();
139
+ const { rerender } = render(
140
+ <InputButton.Root>
141
+ <InputButton.Button accessibility-label="Select" bindtap={onOpen} />
142
+ <InputButton.Value>Value</InputButton.Value>
143
+ <InputButton.ClearButton
144
+ icon={<MockIcon />}
145
+ accessibility-label="Clear"
146
+ bindtap={onClear}
147
+ />
148
+ </InputButton.Root>,
149
+ );
150
+ const root = getRenderedRoot();
151
+ const clear = root.querySelector(".seed-input-button__clearButton") as HTMLElement;
152
+ expect(clear).toHaveAttribute("flatten", "false");
153
+ fireEvent.tap(clear);
154
+ expect(onClear).toHaveBeenCalledTimes(1);
155
+ expect(onOpen).not.toHaveBeenCalled();
156
+ expect(root.querySelector(".seed-input-button__button")).not.toHaveClass(
157
+ "seed-input-button__button--pressed_true",
158
+ );
159
+
160
+ rerender(
161
+ <InputButton.Root readOnly>
162
+ <InputButton.Button accessibility-label="Select" bindtap={onOpen} />
163
+ <InputButton.ClearButton
164
+ icon={<MockIcon />}
165
+ accessibility-label="Clear"
166
+ bindtap={onClear}
167
+ />
168
+ </InputButton.Root>,
169
+ );
170
+ expect(getRenderedRoot().querySelector(".seed-input-button__clearButton")).toBeNull();
171
+ });
172
+
173
+ it("bridges Main Thread presses while preserving the consumer Main Thread handler", async () => {
174
+ function Example({ report }: { report: () => void }) {
175
+ function handleTouch() {
176
+ "main thread";
177
+ runOnBackground(report)();
178
+ }
179
+ return (
180
+ <InputButton.Root>
181
+ <InputButton.Button
182
+ accessibility-label="Select"
183
+ main-thread:bindtouchstart={handleTouch}
184
+ />
185
+ <InputButton.Value>Value</InputButton.Value>
186
+ </InputButton.Root>
187
+ );
188
+ }
189
+ const report = vi.fn();
190
+ const { container } = render(<Example report={report} />, {
191
+ enableMainThread: true,
192
+ enableBackgroundThread: true,
193
+ });
194
+ await waitSchedule();
195
+ const button = container.querySelector(".seed-input-button__button");
196
+ if (!button) throw new Error("Expected button");
197
+ fireEvent.touchstart(button, {});
198
+ await waitSchedule();
199
+ expect(report).toHaveBeenCalledTimes(1);
200
+ expect(button.className).toContain("pressed_true");
201
+ fireEvent.touchcancel(button, {});
202
+ await waitSchedule();
203
+ expect(button.className).not.toContain("pressed_true");
204
+ });
205
+
79
206
  it("throws when a slot is rendered outside InputButton.Root", () => {
80
207
  expect(() => render(<InputButton.Value>값</InputButton.Value>)).toThrow(/InputButton\.Value/);
81
208
  });
@@ -7,6 +7,9 @@ import {
7
7
  import clsx from "clsx";
8
8
 
9
9
  import { usePressTap } from "../../hooks/usePressTap";
10
+ import { useScaleFeedback, type ScaleFeedbackTriggerProps } from "../../hooks/useScaleFeedback";
11
+ import { mergeProps } from "../../utils/merge-props";
12
+ import { toArray } from "../../utils/children";
10
13
  import type {
11
14
  LynxAccessibilityProps,
12
15
  LynxPressableProps,
@@ -20,6 +23,8 @@ interface InputButtonContextValue {
20
23
  variantProps: InputButtonVariantProps;
21
24
  disabled: boolean;
22
25
  readOnly: boolean;
26
+ pressed: boolean;
27
+ scaleFeedbackTriggerProps: ScaleFeedbackTriggerProps;
23
28
  }
24
29
 
25
30
  const InputButtonContext = React.createContext<InputButtonContextValue | null>(null);
@@ -42,6 +47,8 @@ function useInputButtonContext(consumer: string): InputButtonContextValue {
42
47
  * 웹 대비 미지원 기능:
43
48
  * - `size="responsive"`: Lynx에는 CSS viewport breakpoint가 없음
44
49
  * - HTML form 제출과 DOM ARIA id 연결
50
+ * - Content Scale을 적용하려면 Button을 Root의 직접 자식 또는 Fragment 안에 둡니다.
51
+ * 커스텀 컴포넌트로 감싼 Button은 기존 구조를 보존하며 Content Scale을 생략합니다.
45
52
  */
46
53
  export interface InputButtonRootProps
47
54
  extends Omit<InputButtonVariantProps, "pressed">,
@@ -54,9 +61,21 @@ export const InputButtonRoot = React.forwardRef<NodesRef, InputButtonRootProps>(
54
61
  const disabled = variantProps.disabled ?? false;
55
62
  const readOnly = variantProps.readOnly ?? false;
56
63
  const classes = inputButton(variantProps);
64
+ const { pressed, bindtouchstart, bindtouchend, bindtouchcancel } = usePressTap({
65
+ disabled: disabled || readOnly,
66
+ });
67
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
68
+ disabled: disabled || readOnly,
69
+ onTouchStart: bindtouchstart,
70
+ onTouchEnd: bindtouchend,
71
+ onTouchCancel: bindtouchcancel,
72
+ });
73
+ const childArray = flattenInputButtonChildren(children);
74
+ const buttonChildren = childArray.filter(isInputButtonButton);
75
+ const contentChildren = childArray.filter((child) => !isInputButtonButton(child));
57
76
  const contextValue = React.useMemo(
58
- () => ({ variantProps, disabled, readOnly }),
59
- [variantProps, disabled, readOnly],
77
+ () => ({ variantProps, disabled, readOnly, pressed, scaleFeedbackTriggerProps }),
78
+ [variantProps, disabled, readOnly, pressed, scaleFeedbackTriggerProps],
60
79
  );
61
80
 
62
81
  return (
@@ -66,7 +85,16 @@ export const InputButtonRoot = React.forwardRef<NodesRef, InputButtonRootProps>(
66
85
  className={clsx(classes.root, className)}
67
86
  {...nativeProps}
68
87
  >
69
- {children}
88
+ {buttonChildren.length > 0 ? (
89
+ <>
90
+ {buttonChildren}
91
+ <view {...scaleFeedbackTargetProps} className={classes.content}>
92
+ {contentChildren}
93
+ </view>
94
+ </>
95
+ ) : (
96
+ children
97
+ )}
70
98
  </view>
71
99
  </InputButtonContext.Provider>
72
100
  );
@@ -93,12 +121,12 @@ export const InputButtonButton = React.forwardRef<unknown, InputButtonButtonProp
93
121
  ...nativeProps
94
122
  } = props;
95
123
  const nonInteractive = context.disabled || context.readOnly;
96
- const { pressed, ...pressHandlers } = usePressTap({
124
+ const { bindtap: handleTap, "main-thread:bindtap": handleMainThreadTap } = usePressTap({
97
125
  disabled: nonInteractive,
98
126
  onTap: bindtap,
99
127
  mainThreadOnTap: mainThreadBindtap,
100
128
  });
101
- const classes = inputButton({ ...context.variantProps, pressed });
129
+ const classes = inputButton({ ...context.variantProps, pressed: context.pressed });
102
130
 
103
131
  return (
104
132
  <view
@@ -106,8 +134,11 @@ export const InputButtonButton = React.forwardRef<unknown, InputButtonButtonProp
106
134
  className={clsx(classes.button, className)}
107
135
  accessibility-element={accessibilityElement}
108
136
  accessibility-traits={accessibilityTraits ?? (nonInteractive ? "disabled" : "button")}
109
- {...pressHandlers}
110
- {...nativeProps}
137
+ {...mergeProps(
138
+ { bindtap: handleTap, "main-thread:bindtap": handleMainThreadTap },
139
+ context.scaleFeedbackTriggerProps,
140
+ nativeProps,
141
+ )}
111
142
  >
112
143
  <view className={classes.baseStroke} accessibility-elements-hidden={true} />
113
144
  <view className={classes.stroke} accessibility-elements-hidden={true} />
@@ -117,6 +148,18 @@ export const InputButtonButton = React.forwardRef<unknown, InputButtonButtonProp
117
148
  });
118
149
  InputButtonButton.displayName = "InputButtonButton";
119
150
 
151
+ function isInputButtonButton(node: React.ReactNode) {
152
+ return React.isValidElement(node) && node.type === InputButtonButton;
153
+ }
154
+
155
+ function flattenInputButtonChildren(children: React.ReactNode): React.ReactNode[] {
156
+ return toArray(children).flatMap((child) =>
157
+ React.isValidElement<{ children?: React.ReactNode }>(child) && child.type === React.Fragment
158
+ ? flattenInputButtonChildren(child.props.children)
159
+ : [child],
160
+ );
161
+ }
162
+
120
163
  ////////////////////////////////////////////////////////////////////////////////////
121
164
 
122
165
  export interface InputButtonValueProps extends LynxStyledElementProps {}
@@ -272,6 +315,9 @@ export const InputButtonClearButton = React.forwardRef<unknown, InputButtonClear
272
315
  mainThreadOnTap: mainThreadBindtap,
273
316
  });
274
317
  const classes = inputButton({ ...context.variantProps, pressed });
318
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
319
+ disabled: nonInteractive,
320
+ });
275
321
 
276
322
  if (nonInteractive) return null;
277
323
 
@@ -286,8 +332,13 @@ export const InputButtonClearButton = React.forwardRef<unknown, InputButtonClear
286
332
  accessibility-element={accessibilityElement}
287
333
  accessibility-label={accessibilityLabel}
288
334
  accessibility-traits="button"
289
- {...pressHandlers}
290
- {...otherProps}
335
+ {...mergeProps(
336
+ pressHandlers,
337
+ scaleFeedbackTriggerProps,
338
+ scaleFeedbackTargetProps,
339
+ otherProps,
340
+ { flatten: false },
341
+ )}
291
342
  />
292
343
  );
293
344
  },
@@ -1,7 +1,9 @@
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
+ import { Checkbox } from "../Checkbox";
6
+ import { Switch } from "../Switch";
5
7
  import { RadioGroup } from "../RadioGroup";
6
8
  import { List, ListHeader } from "./index";
7
9
 
@@ -66,7 +68,7 @@ describe("List", () => {
66
68
  );
67
69
  });
68
70
 
69
- it("tracks button pressed state and ignores a disabled tap", () => {
71
+ it("tracks button pressed state and ignores a disabled tap", async () => {
70
72
  const onTap = vi.fn();
71
73
  const { rerender } = render(
72
74
  <List.ButtonItem className="button-item" bindtap={onTap} accessibility-label="열기">
@@ -74,15 +76,19 @@ describe("List", () => {
74
76
  <List.Title>버튼</List.Title>
75
77
  </List.Content>
76
78
  </List.ButtonItem>,
79
+ { enableMainThread: true, enableBackgroundThread: true },
77
80
  );
78
81
 
82
+ await waitSchedule();
79
83
  let item = getListItem("button-item");
80
84
  fireEvent.touchstart(item, {});
85
+ await waitSchedule();
81
86
  expect(item.querySelector(".seed-list-item__pressedOverlay")).toHaveClass(
82
87
  "seed-list-item__pressedOverlay--pressed_true",
83
88
  );
84
89
 
85
90
  fireEvent.tap(item);
91
+ await waitSchedule();
86
92
  expect(onTap).toHaveBeenCalledTimes(1);
87
93
 
88
94
  rerender(
@@ -94,6 +100,7 @@ describe("List", () => {
94
100
  );
95
101
  item = getListItem("button-item");
96
102
  fireEvent.tap(item);
103
+ await waitSchedule();
97
104
 
98
105
  expect(onTap).toHaveBeenCalledTimes(1);
99
106
  expect(item).toHaveAttribute("accessibility-traits", "disabled");
@@ -159,4 +166,49 @@ describe("List", () => {
159
166
  expect(first).toHaveAttribute("accessibility-value", "선택 안 됨");
160
167
  expect(second).toHaveAttribute("accessibility-value", "선택됨");
161
168
  });
169
+ it("scales only interactive row content and suppresses nested control targets", () => {
170
+ render(
171
+ <List.Root>
172
+ <List.Item className="static-row">
173
+ <List.Title>Static</List.Title>
174
+ </List.Item>
175
+ <List.CheckboxItem className="check-row">
176
+ <List.Prefix>
177
+ <Checkbox.Control />
178
+ </List.Prefix>
179
+ <List.Title>Check</List.Title>
180
+ </List.CheckboxItem>
181
+ <List.SwitchItem className="switch-row">
182
+ <List.Suffix>
183
+ <Switch.Control>
184
+ <Switch.Thumb />
185
+ </Switch.Control>
186
+ </List.Suffix>
187
+ </List.SwitchItem>
188
+ <RadioGroup.Root>
189
+ <List.RadioItem value="a" className="radio-row">
190
+ <List.Prefix>
191
+ <RadioGroup.ItemControl />
192
+ </List.Prefix>
193
+ </List.RadioItem>
194
+ </RadioGroup.Root>
195
+ </List.Root>,
196
+ );
197
+ expect(getListItem("static-row").querySelector(".seed-list-item__layout")).not.toHaveAttribute(
198
+ "flatten",
199
+ "false",
200
+ );
201
+ for (const name of ["check-row", "switch-row", "radio-row"]) {
202
+ const row = getListItem(name);
203
+ const layout = row.querySelector(".seed-list-item__layout")!;
204
+ expect(layout).toHaveAttribute("flatten", "false");
205
+ expect(layout).not.toContainElement(row.querySelector(".seed-list-item__pressedOverlay"));
206
+ if (name === "switch-row") {
207
+ // Switch keeps its native view for its own thumb animation even without a scale target.
208
+ expect(layout.querySelector(".seed-switchmark__root")).toHaveAttribute("flatten", "false");
209
+ } else {
210
+ expect(layout.querySelectorAll('[flatten="false"]')).toHaveLength(0);
211
+ }
212
+ }
213
+ });
162
214
  });