@hanzogui/kitchen-sink 3.0.5

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 (431) hide show
  1. package/.detoxrc.js +130 -0
  2. package/.env.production +2 -0
  3. package/.maestro/config.yaml +4 -0
  4. package/.maestro/flows/shorthand-variables.yaml +23 -0
  5. package/.watchmanconfig +1 -0
  6. package/LICENSE +21 -0
  7. package/README.md +11 -0
  8. package/app.json +43 -0
  9. package/assets/adaptive-icon.png +0 -0
  10. package/assets/favicon.png +0 -0
  11. package/assets/icon.png +0 -0
  12. package/assets/splash.png +0 -0
  13. package/babel.config.js +25 -0
  14. package/e2e/CompilerExtraction.test.ts +147 -0
  15. package/e2e/GroupPressNative.test.ts +167 -0
  16. package/e2e/MediaQueryGtMd.test.ts +71 -0
  17. package/e2e/NativePortal.test.ts +113 -0
  18. package/e2e/PointerEvents.test.ts +116 -0
  19. package/e2e/PressStyleNative.noRngh.test.ts +191 -0
  20. package/e2e/PressStyleNative.test.ts +231 -0
  21. package/e2e/SafeArea.test.ts +57 -0
  22. package/e2e/SelectAndroidOnPress.test.ts +181 -0
  23. package/e2e/SelectRemount.test.ts +137 -0
  24. package/e2e/SheetDragResist.test.ts +370 -0
  25. package/e2e/SheetKeyboardDrag.test.ts +249 -0
  26. package/e2e/SheetScrollableDrag.test.ts +560 -0
  27. package/e2e/ShorthandVariables.test.ts +53 -0
  28. package/e2e/ThemeChangeBasic.test.ts +123 -0
  29. package/e2e/ThemeMutation.test.ts +80 -0
  30. package/e2e/check-rngh-status.test.ts +31 -0
  31. package/e2e/jest.config.js +19 -0
  32. package/e2e/utils/colors.ts +75 -0
  33. package/e2e/utils/navigation.ts +53 -0
  34. package/eas.json +22 -0
  35. package/flows/AlertDialog.yaml +17 -0
  36. package/flows/OpenApp.yaml +25 -0
  37. package/flows/Select.yaml +13 -0
  38. package/flows/Sheet.yaml +12 -0
  39. package/flows/Tabs.yaml +13 -0
  40. package/flows/Toast.yaml +14 -0
  41. package/flows/WarmUp.yaml +24 -0
  42. package/index.html +21 -0
  43. package/index.js +17 -0
  44. package/metro.config.js +64 -0
  45. package/next-router-shim.ts +9 -0
  46. package/package.json +118 -0
  47. package/plans/toast-2.md +471 -0
  48. package/playwright.config.ts +71 -0
  49. package/plugins/expo-modules-core-swift6.js +76 -0
  50. package/pod-install.sh +7 -0
  51. package/public/favicon.svg +70 -0
  52. package/public/fonts/inter.css +15 -0
  53. package/public/fonts/noto-cn.otf +0 -0
  54. package/public/gui-icon.svg +68 -0
  55. package/run-detox.sh +230 -0
  56. package/run-native-tests.sh +4 -0
  57. package/run-tests-parallel.ts +195 -0
  58. package/screenshots/Screenshotter.test.tsx +48 -0
  59. package/src/AnimationDemos.tsx +131 -0
  60. package/src/App.native.tsx +121 -0
  61. package/src/App.tsx +121 -0
  62. package/src/Navigation.tsx +98 -0
  63. package/src/Sandbox.tsx +87 -0
  64. package/src/TestDynamicEval.tsx +33 -0
  65. package/src/TestNativeSheet.tsx +100 -0
  66. package/src/components/TimedRender.tsx +18 -0
  67. package/src/constants/test-ids.ts +52 -0
  68. package/src/features/demos/demo-screen.tsx +72 -0
  69. package/src/features/home/ColorSchemeListItem.tsx +41 -0
  70. package/src/features/home/TestBuildAButton.tsx +102 -0
  71. package/src/features/home/TestSeparator.tsx +0 -0
  72. package/src/features/home/screen.tsx +285 -0
  73. package/src/features/testcases/screen.tsx +59 -0
  74. package/src/features/testcases/test-screen.tsx +50 -0
  75. package/src/generatedV5Theme.ts +112 -0
  76. package/src/gui.config.ts +411 -0
  77. package/src/guy.png +0 -0
  78. package/src/index.tsx +6 -0
  79. package/src/provider/index.tsx +18 -0
  80. package/src/test-gui-stack.tsx +11 -0
  81. package/src/test.tsx +3 -0
  82. package/src/useKitchenSinkTheme.tsx +15 -0
  83. package/src/usecases/ActionsSheetComparison.tsx +194 -0
  84. package/src/usecases/AnimatePresenceEnterExitCase.tsx +255 -0
  85. package/src/usecases/AnimatePresenceExitTest.tsx +69 -0
  86. package/src/usecases/AnimatedByProp.tsx +39 -0
  87. package/src/usecases/AnimationComprehensiveCase.tsx +2515 -0
  88. package/src/usecases/AnimationValueLoggingCase.tsx +526 -0
  89. package/src/usecases/AnimationsWithMediaQueriesCase.tsx +110 -0
  90. package/src/usecases/Benchmark.tsx +148 -0
  91. package/src/usecases/BenchmarkSelect.tsx +34 -0
  92. package/src/usecases/ButtonCircular.tsx +3 -0
  93. package/src/usecases/ButtonCustom.tsx +33 -0
  94. package/src/usecases/ButtonIconColor.tsx +18 -0
  95. package/src/usecases/ButtonInverse.tsx +30 -0
  96. package/src/usecases/ButtonUnstyled.tsx +31 -0
  97. package/src/usecases/CheckboxDisabledOnPress.tsx +62 -0
  98. package/src/usecases/ClickDuringEnterCase.tsx +59 -0
  99. package/src/usecases/CodeExamplesInput.tsx +9 -0
  100. package/src/usecases/ColorTokenFallback.tsx +52 -0
  101. package/src/usecases/CompilerExtraction.tsx +380 -0
  102. package/src/usecases/ComplexVariants.tsx +164 -0
  103. package/src/usecases/CrashAdaptSheet.tsx +98 -0
  104. package/src/usecases/CustomStyledAnimatedPopover.tsx +42 -0
  105. package/src/usecases/CustomStyledAnimatedTooltip.tsx +72 -0
  106. package/src/usecases/DOMNodeAPIs.tsx +154 -0
  107. package/src/usecases/DialogFocusScopeCase.tsx +277 -0
  108. package/src/usecases/DialogFocusScopeDebug.tsx +85 -0
  109. package/src/usecases/DialogNestedCase.tsx +121 -0
  110. package/src/usecases/DialogOpenControlled.tsx +49 -0
  111. package/src/usecases/DialogPointerEventsCase.tsx +58 -0
  112. package/src/usecases/DialogScopedCase.tsx +106 -0
  113. package/src/usecases/DialogSheetAdaptCase.tsx +178 -0
  114. package/src/usecases/DialogSheetAdaptResizeCase.tsx +98 -0
  115. package/src/usecases/DismissLayerStackingCase.tsx +223 -0
  116. package/src/usecases/DriverDisableAnimationPropsCase.tsx +44 -0
  117. package/src/usecases/Example.tsx +10 -0
  118. package/src/usecases/ExitCompletionCase.tsx +713 -0
  119. package/src/usecases/FocusVisibleButton.tsx +14 -0
  120. package/src/usecases/FocusVisibleButtonPointer.tsx +13 -0
  121. package/src/usecases/FocusVisibleButtonWithFocusStyle.tsx +16 -0
  122. package/src/usecases/FocusWithinCase.tsx +55 -0
  123. package/src/usecases/FontTokensInVariants.tsx +14 -0
  124. package/src/usecases/FormButtonTypeCase.tsx +34 -0
  125. package/src/usecases/GlobalScopedTriggerIsolationCase.tsx +178 -0
  126. package/src/usecases/GroupHoverMobile.tsx +39 -0
  127. package/src/usecases/GroupPressInVariant.tsx +92 -0
  128. package/src/usecases/GroupPressNative.tsx +200 -0
  129. package/src/usecases/GroupProp.tsx +96 -0
  130. package/src/usecases/GroupPseudoVariantOverride.tsx +56 -0
  131. package/src/usecases/GroupUseCases.tsx +94 -0
  132. package/src/usecases/HeightMediaQueryOverrideCase.tsx +183 -0
  133. package/src/usecases/InputAutoFocusAfterMenuCase.tsx +105 -0
  134. package/src/usecases/InputAutoFocusStyledCase.tsx +39 -0
  135. package/src/usecases/KeyboardControllerTest.tsx +146 -0
  136. package/src/usecases/ListItem.tsx +123 -0
  137. package/src/usecases/MediaQueriesV5.tsx +137 -0
  138. package/src/usecases/MediaQueryGtMd.tsx +73 -0
  139. package/src/usecases/MenuAboveDialogCase.tsx +75 -0
  140. package/src/usecases/MenuAccessibilityCase.tsx +133 -0
  141. package/src/usecases/MenuAnimatePositionCase.tsx +41 -0
  142. package/src/usecases/MenuArrowAnimatePresenceCase.tsx +98 -0
  143. package/src/usecases/MenuAsChildPositionCase.tsx +24 -0
  144. package/src/usecases/MenuAutoResizeCase.tsx +57 -0
  145. package/src/usecases/MenuBottomCase.tsx +55 -0
  146. package/src/usecases/MenuFocusLeaveCase.tsx +135 -0
  147. package/src/usecases/MenuHighlightCase.tsx +44 -0
  148. package/src/usecases/MenuItemFocusCase.tsx +79 -0
  149. package/src/usecases/MenuItemPseudoOverrideCase.tsx +270 -0
  150. package/src/usecases/MenuMultiTriggerCase.tsx +47 -0
  151. package/src/usecases/MenuOverflowCase.tsx +60 -0
  152. package/src/usecases/MenuSubCase.tsx +223 -0
  153. package/src/usecases/MenuSubLeftCase.tsx +178 -0
  154. package/src/usecases/MenuSubNestedPositionCase.tsx +171 -0
  155. package/src/usecases/MenuSubStyledCase.tsx +145 -0
  156. package/src/usecases/MenuThemeCase.tsx +50 -0
  157. package/src/usecases/MenuUnstyledCase.tsx +52 -0
  158. package/src/usecases/MultiDriverAnimation.tsx +118 -0
  159. package/src/usecases/NativePortalTest.tsx +179 -0
  160. package/src/usecases/NewInputBasic.tsx +16 -0
  161. package/src/usecases/NewInputEvents.tsx +29 -0
  162. package/src/usecases/NonGuiTextStyledType.tsx +23 -0
  163. package/src/usecases/OnLayoutCase.tsx +134 -0
  164. package/src/usecases/OnLayoutScaleCase.tsx +88 -0
  165. package/src/usecases/OnLayoutStressCase.tsx +353 -0
  166. package/src/usecases/OpacityModifierCase.tsx +113 -0
  167. package/src/usecases/OverlayStyled.tsx +66 -0
  168. package/src/usecases/ParagraphSpanFontInheritance.tsx +53 -0
  169. package/src/usecases/PlaceholderTextColor.tsx +20 -0
  170. package/src/usecases/PointerEventsCase.tsx +100 -0
  171. package/src/usecases/PopoverAndMenuMultiTriggerCase.tsx +138 -0
  172. package/src/usecases/PopoverCase.tsx +222 -0
  173. package/src/usecases/PopoverContentStyledPlusAnimations.tsx +44 -0
  174. package/src/usecases/PopoverFocusScopeCase.tsx +171 -0
  175. package/src/usecases/PopoverHoverableCase.tsx +167 -0
  176. package/src/usecases/PopoverHoverableDisableClickCase.tsx +118 -0
  177. package/src/usecases/PopoverHoverableRapidCase.tsx +103 -0
  178. package/src/usecases/PopoverHoverableScopedCase.tsx +135 -0
  179. package/src/usecases/PopoverScopedCase.tsx +76 -0
  180. package/src/usecases/PopoverTriggerIsolationCase.tsx +80 -0
  181. package/src/usecases/PressStyleNative.tsx +143 -0
  182. package/src/usecases/PseudoStyleMerge.tsx +25 -0
  183. package/src/usecases/PseudoTransitionCase.tsx +174 -0
  184. package/src/usecases/RawAnimatedValueCase.tsx +231 -0
  185. package/src/usecases/RemoveScrollCase.tsx +66 -0
  186. package/src/usecases/RenderPropCase.tsx +263 -0
  187. package/src/usecases/SafeAreaCase.tsx +236 -0
  188. package/src/usecases/ScrollViewRefCase.tsx +88 -0
  189. package/src/usecases/SecondPage.tsx +5 -0
  190. package/src/usecases/SelectAndroidOnPress.tsx +129 -0
  191. package/src/usecases/SelectFocusScopeCase.tsx +270 -0
  192. package/src/usecases/SelectRemount.tsx +136 -0
  193. package/src/usecases/Shadows.tsx +5 -0
  194. package/src/usecases/SheetAnimationCase.tsx +155 -0
  195. package/src/usecases/SheetDragCase.tsx +183 -0
  196. package/src/usecases/SheetDragResistCase.tsx +433 -0
  197. package/src/usecases/SheetDragResistCase.web.tsx +359 -0
  198. package/src/usecases/SheetKeyboardDragCase.tsx +328 -0
  199. package/src/usecases/SheetKeyboardFitContentCase.tsx +165 -0
  200. package/src/usecases/SheetOnAnimationCompleteCase.tsx +54 -0
  201. package/src/usecases/SheetScrollLockCase.tsx +166 -0
  202. package/src/usecases/SheetScrollableDrag.tsx +249 -0
  203. package/src/usecases/SheetSnapPointsFitCase.tsx +393 -0
  204. package/src/usecases/ShorthandVariables.tsx +49 -0
  205. package/src/usecases/SlowThemeReRender.tsx +48 -0
  206. package/src/usecases/SpinnerCustomColors.tsx +34 -0
  207. package/src/usecases/StackZIndex.tsx +82 -0
  208. package/src/usecases/StressPage.tsx +301 -0
  209. package/src/usecases/StylePlatform.tsx +30 -0
  210. package/src/usecases/StyleProp.tsx +29 -0
  211. package/src/usecases/StyledAnchor.tsx +27 -0
  212. package/src/usecases/StyledButtonAnimationAuto.tsx +99 -0
  213. package/src/usecases/StyledButtonTheme.tsx +63 -0
  214. package/src/usecases/StyledButtonVariantPseudo.tsx +25 -0
  215. package/src/usecases/StyledButtonVariantPseudoMerge.tsx +77 -0
  216. package/src/usecases/StyledCheckboxTheme.tsx +23 -0
  217. package/src/usecases/StyledContextColor.tsx +246 -0
  218. package/src/usecases/StyledContextTokens.tsx +147 -0
  219. package/src/usecases/StyledHOCNamed.tsx +20 -0
  220. package/src/usecases/StyledHtmlCase.tsx +144 -0
  221. package/src/usecases/StyledIconColor.tsx +19 -0
  222. package/src/usecases/StyledInputFocusStyle.tsx +21 -0
  223. package/src/usecases/StyledInputOnFocus.tsx +30 -0
  224. package/src/usecases/StyledMediaQueryMerge.tsx +95 -0
  225. package/src/usecases/StyledOverridePsuedo.tsx +26 -0
  226. package/src/usecases/StyledRNW.tsx +61 -0
  227. package/src/usecases/StyledStyleableInputOnFocus.tsx +34 -0
  228. package/src/usecases/StyledStyleableInputVariant.tsx +48 -0
  229. package/src/usecases/StyledStyledStyleableInputOnFocus.tsx +36 -0
  230. package/src/usecases/StyledVariantTextColor.tsx +25 -0
  231. package/src/usecases/StyledViewOnFocus.tsx +32 -0
  232. package/src/usecases/TabHoverAnimationCase.tsx +212 -0
  233. package/src/usecases/TextNestedInheritance.tsx +80 -0
  234. package/src/usecases/ThemeChange.tsx +100 -0
  235. package/src/usecases/ThemeChangeBasic.tsx +52 -0
  236. package/src/usecases/ThemeComponentResolution.tsx +119 -0
  237. package/src/usecases/ThemeConditionalName.tsx +31 -0
  238. package/src/usecases/ThemeMediaAnimationCase.tsx +39 -0
  239. package/src/usecases/ThemeMutation.tsx +86 -0
  240. package/src/usecases/ThemeNested.tsx +103 -0
  241. package/src/usecases/ThemeReset.tsx +62 -0
  242. package/src/usecases/ThemeShallowCase.tsx +83 -0
  243. package/src/usecases/ToastCase.tsx +46 -0
  244. package/src/usecases/ToggleGroupActiveProps.tsx +40 -0
  245. package/src/usecases/ToggleGroupXGroupCase.tsx +104 -0
  246. package/src/usecases/TooltipAnimationCase.tsx +99 -0
  247. package/src/usecases/TooltipCase.tsx +32 -0
  248. package/src/usecases/TooltipGlobalPatternCase.tsx +83 -0
  249. package/src/usecases/TooltipGroupCase.tsx +102 -0
  250. package/src/usecases/TooltipMultiTriggerCase.tsx +88 -0
  251. package/src/usecases/TooltipPositionJumpCase.tsx +91 -0
  252. package/src/usecases/TooltipTriggerInlineCase.tsx +60 -0
  253. package/src/usecases/TransformMediaQueryMerge.tsx +98 -0
  254. package/src/usecases/UseCases.tsx +409 -0
  255. package/src/usecases/UseTheme.tsx +41 -0
  256. package/src/usecases/V5ThemeBuilderOutput.tsx +231 -0
  257. package/src/usecases/VariantFontFamily.tsx +25 -0
  258. package/src/usecases/VariantsOrder.tsx +117 -0
  259. package/src/usecases/ZIndex.tsx +155 -0
  260. package/src/usecases/helpers.tsx +44 -0
  261. package/src/usecases/index.native.ts +122 -0
  262. package/src/usecases/index.ts +3 -0
  263. package/src/usecases/index.web.ts +177 -0
  264. package/tests/AnimatePresenceEnterExit.animated.test.tsx +176 -0
  265. package/tests/AnimatedByProp.animated.test.tsx +138 -0
  266. package/tests/AnimationBehavior.animated.test.tsx +543 -0
  267. package/tests/AnimationTiming.animated.test.tsx +195 -0
  268. package/tests/AnimationsWithMediaQueries.animated.test.tsx +154 -0
  269. package/tests/BuildAButton.test.tsx +87 -0
  270. package/tests/ButtonCircular.test.tsx +17 -0
  271. package/tests/ButtonCustom.test.tsx +17 -0
  272. package/tests/ButtonIconColor.test.tsx +23 -0
  273. package/tests/ButtonUnstyled.test.tsx +56 -0
  274. package/tests/ClickDuringEnter.animated.test.tsx +174 -0
  275. package/tests/ColorTokenFallback.test.tsx +45 -0
  276. package/tests/DOMNodeAPIs.test.tsx +161 -0
  277. package/tests/DialogFocusScope.animated.test.tsx +309 -0
  278. package/tests/DialogNested.test.tsx +128 -0
  279. package/tests/DialogOpenControlled.test.tsx +42 -0
  280. package/tests/DialogPointerEvents.animated.test.tsx +108 -0
  281. package/tests/DialogScoped.test.tsx +137 -0
  282. package/tests/DialogSheetAdapt.test.tsx +68 -0
  283. package/tests/DialogSheetAdaptResize.test.tsx +161 -0
  284. package/tests/DismissLayerStacking.test.tsx +292 -0
  285. package/tests/DriverDisableAnimationProps.animated.test.tsx +157 -0
  286. package/tests/ExitCompletion.animated.test.tsx +425 -0
  287. package/tests/ExitTimingCheck.animated.test.ts +34 -0
  288. package/tests/FocusVisibleButton.test.tsx +41 -0
  289. package/tests/FocusVisibleButtonPointerFocus.test.tsx +23 -0
  290. package/tests/FocusVisibleButtonPointerFocusWithFocusStyle.test.tsx +40 -0
  291. package/tests/FocusWithinStyle.animated.test.tsx +66 -0
  292. package/tests/FocusWithinStyle.test.tsx +60 -0
  293. package/tests/FormButtonType.test.tsx +42 -0
  294. package/tests/GlobalScopedTriggerIsolation.test.tsx +89 -0
  295. package/tests/GroupHoverMobile.test.tsx +52 -0
  296. package/tests/GroupPressInVariant.test.tsx +82 -0
  297. package/tests/GroupProp.test.tsx +30 -0
  298. package/tests/GroupPseudoVariantOverride.test.tsx +57 -0
  299. package/tests/GroupUseCases.test.tsx +111 -0
  300. package/tests/GuiSiteMotion.test.ts +481 -0
  301. package/tests/HeightMediaQueryOverride.test.tsx +112 -0
  302. package/tests/InputAutoFocusAfterMenu.test.tsx +55 -0
  303. package/tests/InputAutoFocusStyled.test.tsx +22 -0
  304. package/tests/ListItem.test.tsx +129 -0
  305. package/tests/MediaQueriesV5.test.tsx +113 -0
  306. package/tests/MediaQueryGtMd.test.tsx +84 -0
  307. package/tests/MenuAboveDialog.test.tsx +108 -0
  308. package/tests/MenuAccessibility.test.tsx +346 -0
  309. package/tests/MenuAnimatePosition.animated.test.tsx +57 -0
  310. package/tests/MenuArrowAnimatePresence.animated.test.tsx +71 -0
  311. package/tests/MenuAsChildPosition.test.tsx +16 -0
  312. package/tests/MenuAutoResize.test.tsx +54 -0
  313. package/tests/MenuFocusLeave.test.tsx +181 -0
  314. package/tests/MenuHighlight.test.tsx +165 -0
  315. package/tests/MenuHoverKeyboardBugs.test.tsx +252 -0
  316. package/tests/MenuItemFocus.test.tsx +59 -0
  317. package/tests/MenuItemPseudoOverride.test.tsx +231 -0
  318. package/tests/MenuMultiTrigger.test.tsx +101 -0
  319. package/tests/MenuOverflow.test.tsx +93 -0
  320. package/tests/MenuStayInFrame.test.tsx +102 -0
  321. package/tests/MenuSubKeyboardFocus.test.tsx +220 -0
  322. package/tests/MenuSubLeftSafePolygon.test.tsx +88 -0
  323. package/tests/MenuSubNestedPosition.test.tsx +48 -0
  324. package/tests/MenuSubSafePolygon.test.tsx +97 -0
  325. package/tests/MenuSubStyled.test.tsx +40 -0
  326. package/tests/MenuTheme.test.tsx +34 -0
  327. package/tests/MenuUnstyled.test.tsx +56 -0
  328. package/tests/MultiDriverAnimation.test.tsx +207 -0
  329. package/tests/NewInputBasic.test.tsx +50 -0
  330. package/tests/NewInputEvents.test.tsx +55 -0
  331. package/tests/OnLayout.test.tsx +163 -0
  332. package/tests/OnLayoutScale.test.tsx +100 -0
  333. package/tests/OnLayoutStress.test.tsx +304 -0
  334. package/tests/ParagraphSpanFontInheritance.test.tsx +73 -0
  335. package/tests/PointerEvents.test.tsx +123 -0
  336. package/tests/Popover.animated.test.tsx +234 -0
  337. package/tests/PopoverAndMenuMultiTrigger.test.tsx +184 -0
  338. package/tests/PopoverAnimatePosition.animated.test.tsx +51 -0
  339. package/tests/PopoverClickDuringEnter.animated.test.tsx +197 -0
  340. package/tests/PopoverFocusScope.test.tsx +242 -0
  341. package/tests/PopoverHoverable.test.tsx +383 -0
  342. package/tests/PopoverHoverableDisableClick.test.tsx +106 -0
  343. package/tests/PopoverHoverableRapid.test.tsx +129 -0
  344. package/tests/PopoverHoverableReposition.test.tsx +111 -0
  345. package/tests/PopoverHoverableScoped.animated.test.tsx +103 -0
  346. package/tests/PopoverHoverableStress.test.tsx +169 -0
  347. package/tests/PopoverInitialPosition.animated.test.tsx +82 -0
  348. package/tests/PopoverMiddlewareSkipRegression.animated.test.tsx +221 -0
  349. package/tests/PopoverScoped.test.tsx +128 -0
  350. package/tests/PopoverScopedPositionGlitch.animated.test.tsx +184 -0
  351. package/tests/PopoverTriggerIsolation.test.tsx +62 -0
  352. package/tests/PseudoTransition.animated.test.tsx +319 -0
  353. package/tests/RawAnimatedValue.test.tsx +147 -0
  354. package/tests/RemoveScroll.test.tsx +223 -0
  355. package/tests/RenderProp.test.tsx +293 -0
  356. package/tests/ScrollViewRef.test.tsx +39 -0
  357. package/tests/SelectClickHold.test.tsx +147 -0
  358. package/tests/SelectFocusScope.test.tsx +176 -0
  359. package/tests/SelectInnerPositioning.test.tsx +82 -0
  360. package/tests/SelectKeyboardNav.test.tsx +173 -0
  361. package/tests/SelectPositioning.test.tsx +56 -0
  362. package/tests/SelectTypeahead.test.tsx +63 -0
  363. package/tests/Shadows.test.tsx +14 -0
  364. package/tests/SheetAnimation.animated.test.tsx +413 -0
  365. package/tests/SheetDrag.animated.test.tsx +223 -0
  366. package/tests/SheetDragResist.animated.test.tsx +393 -0
  367. package/tests/SheetOnAnimationComplete.animated.test.tsx +62 -0
  368. package/tests/SheetScrollLock.animated.test.tsx +287 -0
  369. package/tests/SheetScrollableDrag.animated.test.tsx +1264 -0
  370. package/tests/SheetSnapPointsFit.animated.test.tsx +259 -0
  371. package/tests/ShorthandVariables.test.tsx +44 -0
  372. package/tests/SpinnerCustomColors.test.tsx +67 -0
  373. package/tests/StackZIndex.test.tsx +51 -0
  374. package/tests/StressPagePerf.test.tsx +76 -0
  375. package/tests/StylePlatform.test.tsx +38 -0
  376. package/tests/StyleProp.test.tsx +20 -0
  377. package/tests/StyledAnchor.test.tsx +17 -0
  378. package/tests/StyledButtonTheme.test.tsx +22 -0
  379. package/tests/StyledButtonVariantPseudo.test.tsx +20 -0
  380. package/tests/StyledButtonVariantPseudoMerge.animated.test.tsx +33 -0
  381. package/tests/StyledCheckboxTheme.test.tsx +16 -0
  382. package/tests/StyledContextColor.test.tsx +119 -0
  383. package/tests/StyledContextTokens.test.tsx +56 -0
  384. package/tests/StyledHOCNamed.test.tsx +16 -0
  385. package/tests/StyledHtml.test.tsx +161 -0
  386. package/tests/StyledIconColor.test.tsx +32 -0
  387. package/tests/StyledInputFocusStyle.test.tsx +19 -0
  388. package/tests/StyledInputOnFocus.test.tsx +27 -0
  389. package/tests/StyledMediaQueryMerge.test.tsx +66 -0
  390. package/tests/StyledRNW.test.tsx +17 -0
  391. package/tests/StyledStyleableInputOnFocus.test.tsx +27 -0
  392. package/tests/StyledStyleableInputVariant.test.tsx +22 -0
  393. package/tests/StyledStyledStyleableInputOnFocus.test.tsx +27 -0
  394. package/tests/StyledVariantTextColor.test.tsx +24 -0
  395. package/tests/StyledViewOnFocus.test.tsx +27 -0
  396. package/tests/TabHoverAnimation.animated.test.tsx +468 -0
  397. package/tests/TabHoverPositionSmooth.animated.test.tsx +129 -0
  398. package/tests/TextNestedInheritance.test.tsx +93 -0
  399. package/tests/ThemeChange.test.tsx +70 -0
  400. package/tests/ThemeComponentResolution.test.tsx +82 -0
  401. package/tests/ThemeConditionalName.test.tsx +34 -0
  402. package/tests/ThemeMediaAnimation.test.tsx +65 -0
  403. package/tests/ThemeNested.test.tsx +141 -0
  404. package/tests/ThemeReset.test.tsx +63 -0
  405. package/tests/ThemeShallow.test.tsx +95 -0
  406. package/tests/Toast.test.tsx +106 -0
  407. package/tests/ToggleGroup.test.tsx +61 -0
  408. package/tests/ToggleGroupActiveProps.test.tsx +38 -0
  409. package/tests/ToggleGroupXGroup.test.tsx +172 -0
  410. package/tests/TooltipAnimation.animated.test.tsx +260 -0
  411. package/tests/TooltipEnterInterrupt.animated.test.tsx +76 -0
  412. package/tests/TooltipGlobalPattern.animated.test.tsx +208 -0
  413. package/tests/TooltipGroup.animated.test.tsx +79 -0
  414. package/tests/TooltipMultiTrigger.test.tsx +116 -0
  415. package/tests/TooltipPositionJump.animated.test.tsx +229 -0
  416. package/tests/TooltipPositionJumpNotes.md +219 -0
  417. package/tests/TooltipRapidSwitch.animated.test.tsx +399 -0
  418. package/tests/TooltipTriggerInline.test.tsx +65 -0
  419. package/tests/TransformMediaQueryMerge.test.tsx +104 -0
  420. package/tests/TransitionEnterExit.animated.test.tsx +311 -0
  421. package/tests/UseTheme.test.tsx +16 -0
  422. package/tests/V5ThemeBuilderOutput.test.tsx +164 -0
  423. package/tests/VariantFontFamily.test.tsx +11 -0
  424. package/tests/VariantsOrder.test.tsx +53 -0
  425. package/tests/_debug_position.mjs +52 -0
  426. package/tests/test-utils.ts +106 -0
  427. package/tests/utils.tsx +54 -0
  428. package/tsconfig.json +45 -0
  429. package/vite-env.d.ts +1 -0
  430. package/vite.config.ts +14 -0
  431. package/webpack.config.js +139 -0
@@ -0,0 +1,174 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+
4
+ /**
5
+ * Tests that clicking to close while an enter animation is still playing
6
+ * doesn't cause a visual snap-back (the element shouldn't jump back to
7
+ * its enterStyle values before starting the exit animation).
8
+ *
9
+ * Bug: motion driver calls controls.stop() before the mid-flight capture,
10
+ * causing the WAAPI animation to cancel and the element to snap back to
11
+ * its pre-animation inline styles for one frame.
12
+ */
13
+
14
+ test.beforeEach(async ({ page }, testInfo) => {
15
+ test.skip(
16
+ testInfo.project.name === 'animated-css',
17
+ 'CSS transitions handle mid-flight interruption differently — this tests WAAPI behavior'
18
+ )
19
+ await setupPage(page, { name: 'ClickDuringEnterCase', type: 'useCase' })
20
+ await page.waitForTimeout(500)
21
+ })
22
+
23
+ test('no opacity snap-back when exit triggered during enter animation', async ({
24
+ page,
25
+ }) => {
26
+ // start per-frame opacity sampling
27
+ await page.evaluate(() => {
28
+ ;(window as any).__opacityLog = [] as number[]
29
+ ;(window as any).__rafRunning = true
30
+ const track = () => {
31
+ if (!(window as any).__rafRunning) return
32
+ const el = document.querySelector('[data-testid="click-enter-target"]')
33
+ if (el) {
34
+ ;(window as any).__opacityLog.push(parseFloat(getComputedStyle(el).opacity))
35
+ }
36
+ requestAnimationFrame(track)
37
+ }
38
+ requestAnimationFrame(track)
39
+ })
40
+
41
+ // show element (starts enter animation: opacity 0 -> 1)
42
+ await page.getByTestId('click-enter-show').click()
43
+ // wait just enough for animation to start (50-100ms into a ~300ms animation)
44
+ await page.waitForTimeout(80)
45
+ // immediately hide (triggers exit while enter is mid-flight)
46
+ await page.getByTestId('click-enter-hide').click()
47
+ // wait for exit animation to complete
48
+ await page.waitForTimeout(1500)
49
+
50
+ // stop sampling
51
+ await page.evaluate(() => {
52
+ ;(window as any).__rafRunning = false
53
+ })
54
+
55
+ const frames: number[] = await page.evaluate(() => (window as any).__opacityLog)
56
+
57
+ // find the peak opacity (the highest opacity reached during enter)
58
+ const peakOpacity = Math.max(...frames)
59
+ const peakIndex = frames.indexOf(peakOpacity)
60
+
61
+ // after the peak, opacity should monotonically decrease (exit animation)
62
+ // but the bug causes a snap-back: opacity jumps DOWN then back UP then down
63
+ // detect this by looking for any frame after peak where opacity increases
64
+ // significantly compared to a previous lower value
65
+
66
+ // simpler check: look for any frame that drops significantly below its
67
+ // neighbors (a "dip" indicates snap-back)
68
+ let maxDip = 0
69
+ for (let i = 1; i < frames.length - 1; i++) {
70
+ const prev = frames[i - 1]
71
+ const curr = frames[i]
72
+ const next = frames[i + 1]
73
+ // a dip is when current is significantly lower than both neighbors
74
+ if (curr < prev && curr < next) {
75
+ const dip = Math.min(prev - curr, next - curr)
76
+ if (dip > maxDip) maxDip = dip
77
+ }
78
+ }
79
+
80
+ // also check for any opacity increase after exit begins
81
+ // find the first frame where opacity starts decreasing after the peak
82
+ let exitStartIndex = peakIndex
83
+ for (let i = peakIndex + 1; i < frames.length; i++) {
84
+ if (frames[i] < frames[i - 1] - 0.01) {
85
+ exitStartIndex = i
86
+ break
87
+ }
88
+ }
89
+
90
+ // after exit starts, look for any significant increase (snap-back indicator)
91
+ let maxIncrease = 0
92
+ for (let i = exitStartIndex + 1; i < frames.length; i++) {
93
+ const increase = frames[i] - frames[i - 1]
94
+ if (increase > maxIncrease) maxIncrease = increase
95
+ }
96
+
97
+ console.log('opacity frames:', JSON.stringify(frames.map((f) => +f.toFixed(3))))
98
+ console.log(`peak: ${peakOpacity.toFixed(3)} at frame ${peakIndex}`)
99
+ console.log(`max dip: ${maxDip.toFixed(3)}`)
100
+ console.log(`max increase after exit: ${maxIncrease.toFixed(3)}`)
101
+
102
+ // there should be no significant dip (snap-back)
103
+ // tolerance: 0.15 allows for minor animation interpolation variance
104
+ expect(
105
+ maxDip,
106
+ `Opacity had a snap-back dip of ${maxDip.toFixed(3)} - element jumped to enterStyle then back. Frames: ${JSON.stringify(frames.slice(0, 30).map((f) => +f.toFixed(2)))}`
107
+ ).toBeLessThan(0.15)
108
+
109
+ // there should be no significant increase after exit starts
110
+ expect(
111
+ maxIncrease,
112
+ `Opacity increased by ${maxIncrease.toFixed(3)} after exit started - indicates snap-back. Frames: ${JSON.stringify(frames.slice(0, 30).map((f) => +f.toFixed(2)))}`
113
+ ).toBeLessThan(0.15)
114
+ })
115
+
116
+ test('no transform snap-back when exit triggered during enter animation', async ({
117
+ page,
118
+ }) => {
119
+ // start per-frame transform sampling
120
+ await page.evaluate(() => {
121
+ ;(window as any).__transformLog = [] as { y: number; scale: number }[]
122
+ ;(window as any).__rafRunning = true
123
+ const track = () => {
124
+ if (!(window as any).__rafRunning) return
125
+ const el = document.querySelector('[data-testid="click-enter-target"]')
126
+ if (el) {
127
+ const transform = getComputedStyle(el).transform
128
+ let y = 0
129
+ let scale = 1
130
+ if (transform && transform !== 'none') {
131
+ const matrix = new DOMMatrix(transform)
132
+ y = matrix.m42 // translateY
133
+ scale = matrix.a // scaleX (assuming uniform)
134
+ }
135
+ ;(window as any).__transformLog.push({ y, scale })
136
+ }
137
+ requestAnimationFrame(track)
138
+ }
139
+ requestAnimationFrame(track)
140
+ })
141
+
142
+ await page.getByTestId('click-enter-show').click()
143
+ await page.waitForTimeout(80)
144
+ await page.getByTestId('click-enter-hide').click()
145
+ await page.waitForTimeout(1500)
146
+
147
+ await page.evaluate(() => {
148
+ ;(window as any).__rafRunning = false
149
+ })
150
+
151
+ const frames: { y: number; scale: number }[] = await page.evaluate(
152
+ () => (window as any).__transformLog
153
+ )
154
+
155
+ // check for Y-position jumps (snap-back to enterStyle y: -10)
156
+ let maxYJump = 0
157
+ for (let i = 1; i < frames.length; i++) {
158
+ const jump = Math.abs(frames[i].y - frames[i - 1].y)
159
+ if (jump > maxYJump) maxYJump = jump
160
+ }
161
+
162
+ console.log(
163
+ 'Y positions:',
164
+ JSON.stringify(frames.slice(0, 30).map((f) => +f.y.toFixed(2)))
165
+ )
166
+ console.log(`max Y jump: ${maxYJump.toFixed(2)}`)
167
+
168
+ // with smooth animation, max per-frame jump should be small
169
+ // a snap-back would show a jump of 5+ pixels in one frame
170
+ expect(
171
+ maxYJump,
172
+ `Y position jumped ${maxYJump.toFixed(1)}px in one frame - indicates snap-back to enterStyle`
173
+ ).toBeLessThan(5)
174
+ })
@@ -0,0 +1,45 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+ import { getStyles } from './utils'
4
+ import { TEST_IDS } from '../src/constants/test-ids'
5
+
6
+ /**
7
+ * Tests for GitHub issue #3620: Theme switching broken after v1.132.15
8
+ *
9
+ * Color tokens should act as fallbacks - if a theme defines a value for the same key,
10
+ * the theme value should take precedence over the color token.
11
+ *
12
+ * The bug was that Object.assign(theme, colorTokens) was overwriting theme values
13
+ * with color token values instead of the other way around.
14
+ */
15
+
16
+ test.beforeEach(async ({ page }) => {
17
+ await setupPage(page, { name: 'ColorTokenFallback', type: 'useCase' })
18
+ })
19
+
20
+ test('theme value takes precedence over color token', async ({ page }) => {
21
+ // The light_ColorTokenTest theme defines customRed as #00ff00 (green)
22
+ // The color token customRed is #ff0000 (red)
23
+ // The theme value should win, so the background should be green
24
+ const square = page.locator(`#${TEST_IDS.colorTokenFallbackThemeValue}`)
25
+ await expect(square).toBeVisible()
26
+
27
+ const styles = await getStyles(square)
28
+ // rgb(0, 255, 0) is #00ff00 (green) - the theme value
29
+ // rgb(255, 0, 0) would be #ff0000 (red) - the color token value (wrong)
30
+ expect(styles.backgroundColor).toBe('rgb(0, 255, 0)')
31
+ })
32
+
33
+ test('color token is used as fallback when theme does not define it', async ({
34
+ page,
35
+ }) => {
36
+ // The light_ColorTokenTest theme does NOT define customBlue
37
+ // The color token customBlue is #0000ff (blue)
38
+ // The color token should be used as a fallback
39
+ const square = page.locator(`#${TEST_IDS.colorTokenFallbackTokenValue}`)
40
+ await expect(square).toBeVisible()
41
+
42
+ const styles = await getStyles(square)
43
+ // rgb(0, 0, 255) is #0000ff (blue) - the color token fallback
44
+ expect(styles.backgroundColor).toBe('rgb(0, 0, 255)')
45
+ })
@@ -0,0 +1,161 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+
4
+ // integration tests for RN 0.82 DOM Node APIs via hanzo-gui component refs
5
+ // verifies that View/Text/ScrollView/Input refs expose the full DOM node API surface
6
+
7
+ test.beforeEach(async ({ page }) => {
8
+ await setupPage(page, { name: 'DOMNodeAPIs', type: 'useCase' })
9
+ // wait for results to render
10
+ await page.waitForSelector('#dom-node-results', { timeout: 5_000 })
11
+ await page.waitForFunction(
12
+ () => {
13
+ const results = document.getElementById('dom-node-results')
14
+ return results && results.children.length > 5
15
+ },
16
+ { timeout: 5_000, polling: 50 }
17
+ )
18
+ })
19
+
20
+ async function getResult(page, key: string): Promise<string> {
21
+ const el = page.locator(`[data-testid="result-${key}"]`)
22
+ const text = await el.textContent()
23
+ // format is "key=value"
24
+ return text?.split('=').slice(1).join('=') ?? ''
25
+ }
26
+
27
+ // tree traversal
28
+
29
+ test('parentNode exists on child ref', async ({ page }) => {
30
+ expect(await getResult(page, 'parentNode-exists')).toBe('true')
31
+ })
32
+
33
+ test('parentElement exists on child ref', async ({ page }) => {
34
+ expect(await getResult(page, 'parentElement-exists')).toBe('true')
35
+ })
36
+
37
+ test('parent.contains(child) returns true', async ({ page }) => {
38
+ expect(await getResult(page, 'parent-contains-childA')).toBe('true')
39
+ expect(await getResult(page, 'parent-contains-childB')).toBe('true')
40
+ })
41
+
42
+ test('child.contains(parent) returns false', async ({ page }) => {
43
+ expect(await getResult(page, 'childA-contains-parent')).toBe('false')
44
+ })
45
+
46
+ test('hasChildNodes returns true', async ({ page }) => {
47
+ expect(await getResult(page, 'hasChildNodes')).toBe('true')
48
+ })
49
+
50
+ test('childNodes and children have length > 0', async ({ page }) => {
51
+ const childNodesLen = Number(await getResult(page, 'childNodes-length'))
52
+ const childrenLen = Number(await getResult(page, 'children-length'))
53
+ expect(childNodesLen).toBeGreaterThan(0)
54
+ expect(childrenLen).toBeGreaterThan(0)
55
+ })
56
+
57
+ test('firstChild and lastChild exist', async ({ page }) => {
58
+ expect(await getResult(page, 'firstChild-exists')).toBe('true')
59
+ expect(await getResult(page, 'lastChild-exists')).toBe('true')
60
+ })
61
+
62
+ test('firstElementChild and lastElementChild exist', async ({ page }) => {
63
+ expect(await getResult(page, 'firstElementChild-exists')).toBe('true')
64
+ expect(await getResult(page, 'lastElementChild-exists')).toBe('true')
65
+ })
66
+
67
+ // sibling traversal
68
+
69
+ test('nextSibling and previousSibling work', async ({ page }) => {
70
+ expect(await getResult(page, 'childA-nextSibling-exists')).toBe('true')
71
+ expect(await getResult(page, 'childB-previousSibling-exists')).toBe('true')
72
+ })
73
+
74
+ test('nextElementSibling and previousElementSibling work', async ({ page }) => {
75
+ expect(await getResult(page, 'childA-nextElementSibling-exists')).toBe('true')
76
+ expect(await getResult(page, 'childB-previousElementSibling-exists')).toBe('true')
77
+ })
78
+
79
+ // getBoundingClientRect
80
+
81
+ test('getBoundingClientRect returns full DOMRect', async ({ page }) => {
82
+ expect(await getResult(page, 'rect-has-width')).toBe('true')
83
+ expect(await getResult(page, 'rect-has-height')).toBe('true')
84
+ expect(await getResult(page, 'rect-has-x')).toBe('true')
85
+ expect(await getResult(page, 'rect-has-y')).toBe('true')
86
+ expect(await getResult(page, 'rect-has-top')).toBe('true')
87
+ expect(await getResult(page, 'rect-has-left')).toBe('true')
88
+ expect(await getResult(page, 'rect-has-right')).toBe('true')
89
+ expect(await getResult(page, 'rect-has-bottom')).toBe('true')
90
+ })
91
+
92
+ // document access
93
+
94
+ test('ownerDocument exists and isConnected is true', async ({ page }) => {
95
+ expect(await getResult(page, 'ownerDocument-exists')).toBe('true')
96
+ expect(await getResult(page, 'isConnected')).toBe('true')
97
+ })
98
+
99
+ test('getElementById via ownerDocument works', async ({ page }) => {
100
+ expect(await getResult(page, 'getElementById-works')).toBe('true')
101
+ })
102
+
103
+ // compareDocumentPosition
104
+
105
+ test('compareDocumentPosition indicates containment', async ({ page }) => {
106
+ expect(await getResult(page, 'compareDocumentPosition-contained')).toBe('true')
107
+ })
108
+
109
+ // node properties
110
+
111
+ test('nodeType is 1 (ELEMENT_NODE)', async ({ page }) => {
112
+ expect(await getResult(page, 'parent-nodeType')).toBe('1')
113
+ })
114
+
115
+ test('nodeName exists', async ({ page }) => {
116
+ expect(await getResult(page, 'parent-nodeName-exists')).toBe('true')
117
+ })
118
+
119
+ // textContent
120
+
121
+ test('textContent returns text from Text component', async ({ page }) => {
122
+ expect(await getResult(page, 'text-textContent')).toBe('hello dom')
123
+ })
124
+
125
+ // offset properties
126
+
127
+ test('offsetWidth and offsetHeight are positive', async ({ page }) => {
128
+ expect(await getResult(page, 'offsetWidth-positive')).toBe('true')
129
+ expect(await getResult(page, 'offsetHeight-positive')).toBe('true')
130
+ })
131
+
132
+ test('offsetParent exists', async ({ page }) => {
133
+ expect(await getResult(page, 'offsetParent-exists')).toBe('true')
134
+ })
135
+
136
+ // client properties
137
+
138
+ test('clientWidth and clientHeight are positive', async ({ page }) => {
139
+ expect(await getResult(page, 'clientWidth-positive')).toBe('true')
140
+ expect(await getResult(page, 'clientHeight-positive')).toBe('true')
141
+ })
142
+
143
+ // childElementCount
144
+
145
+ test('childElementCount is > 0', async ({ page }) => {
146
+ const count = Number(await getResult(page, 'childElementCount'))
147
+ expect(count).toBeGreaterThan(0)
148
+ })
149
+
150
+ // getRootNode
151
+
152
+ test('getRootNode returns a node', async ({ page }) => {
153
+ expect(await getResult(page, 'getRootNode-exists')).toBe('true')
154
+ })
155
+
156
+ // focus/blur
157
+
158
+ test('focus and blur work on Input ref', async ({ page }) => {
159
+ expect(await getResult(page, 'focus-works')).toBe('true')
160
+ expect(await getResult(page, 'blur-works')).toBe('true')
161
+ })
@@ -0,0 +1,309 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+
4
+ test.describe('Dialog Focus Scope', () => {
5
+ test.beforeEach(async ({ page }) => {
6
+ await setupPage(page, { name: 'DialogFocusScopeCase', type: 'useCase' })
7
+ })
8
+
9
+ test('traps focus within dialog when modal', async ({ page }) => {
10
+ await page.waitForLoadState('networkidle')
11
+
12
+ // Open the modal dialog
13
+ const trigger = page.getByTestId('modal-dialog-trigger')
14
+ await trigger.click()
15
+
16
+ // Wait for dialog to be visible
17
+ const dialogContent = page.getByTestId('modal-dialog-content')
18
+ await expect(dialogContent).toBeVisible({ timeout: 5000 })
19
+
20
+ // Focus should be on the first input
21
+ // Wait for focus trap to activate (no idle wait now)
22
+ await page.waitForTimeout(300)
23
+
24
+ const firstInput = dialogContent.getByTestId('first-input')
25
+ await expect(firstInput).toBeFocused()
26
+
27
+ // Tab through all focusable elements in order
28
+ await page.keyboard.press('Tab')
29
+ const secondInput = dialogContent.getByTestId('second-input')
30
+ await expect(secondInput).toBeFocused()
31
+
32
+ await page.keyboard.press('Tab')
33
+ const emailInput = dialogContent.getByTestId('email-input')
34
+ await expect(emailInput).toBeFocused()
35
+
36
+ await page.keyboard.press('Tab')
37
+ const countrySelect = dialogContent.getByTestId('country-select')
38
+ await expect(countrySelect).toBeFocused()
39
+
40
+ await page.keyboard.press('Tab')
41
+ const commentsTextarea = dialogContent.getByTestId('comments-textarea')
42
+ await expect(commentsTextarea).toBeFocused()
43
+
44
+ await page.keyboard.press('Tab')
45
+ const termsCheckbox = dialogContent.getByTestId('terms-checkbox')
46
+ await expect(termsCheckbox).toBeFocused()
47
+
48
+ await page.keyboard.press('Tab')
49
+ const cancelButton = dialogContent.getByTestId('cancel-button')
50
+ await expect(cancelButton).toBeFocused()
51
+
52
+ await page.keyboard.press('Tab')
53
+ const saveButton = dialogContent.getByTestId('save-button')
54
+ await expect(saveButton).toBeFocused()
55
+
56
+ // Tab should wrap back to first input (loop)
57
+ await page.keyboard.press('Tab')
58
+ await expect(firstInput).toBeFocused()
59
+
60
+ // Shift+Tab should go backwards and loop - from first input to last button
61
+ await page.keyboard.press('Shift+Tab')
62
+ await expect(saveButton).toBeFocused()
63
+
64
+ await page.keyboard.press('Shift+Tab')
65
+ await expect(cancelButton).toBeFocused()
66
+
67
+ // Close dialog with Escape key instead of clicking
68
+ await page.keyboard.press('Escape')
69
+ await expect(dialogContent).not.toBeVisible()
70
+
71
+ // Focus should return to trigger
72
+ await expect(trigger).toBeFocused()
73
+ })
74
+
75
+ test('does not trap focus when non-modal', async ({ page }) => {
76
+ await page.waitForLoadState('networkidle')
77
+
78
+ // Open the non-modal dialog
79
+ const trigger = page.getByTestId('non-modal-dialog-trigger')
80
+ await trigger.click()
81
+
82
+ // Wait for dialog to be visible
83
+ const dialogContent = page.getByTestId('non-modal-dialog-content')
84
+ await expect(dialogContent).toBeVisible({ timeout: 5000 })
85
+
86
+ // Focus on the first input
87
+ const firstInput = dialogContent.getByTestId('first-input')
88
+ await firstInput.focus()
89
+ await expect(firstInput).toBeFocused()
90
+
91
+ // Tab should allow focus to leave the dialog
92
+ await page.keyboard.press('Tab')
93
+ await page.keyboard.press('Tab')
94
+ await page.keyboard.press('Tab')
95
+ await page.keyboard.press('Tab')
96
+ await page.keyboard.press('Tab') // Tab past all dialog elements
97
+
98
+ // Focus should have left the dialog
99
+ const focusedElement = await page.evaluate(() => document.activeElement?.tagName)
100
+ expect(focusedElement).toBeTruthy()
101
+
102
+ // Click outside should close non-modal dialog
103
+ await page.click('body', { position: { x: 10, y: 10 } })
104
+ await expect(dialogContent).not.toBeVisible()
105
+ })
106
+
107
+ test('modal dialogs prevent right-click dismiss but allow left-click dismiss', async ({
108
+ page,
109
+ }) => {
110
+ await page.waitForLoadState('networkidle')
111
+
112
+ // Open a modal dialog
113
+ const trigger = page.getByTestId('modal-dialog-trigger')
114
+ await trigger.click()
115
+
116
+ const dialogContent = page.getByTestId('modal-dialog-content')
117
+ await expect(dialogContent).toBeVisible({ timeout: 5000 })
118
+
119
+ // Wait for auto-focus
120
+ await page.waitForTimeout(300)
121
+
122
+ // Try to right-click outside the dialog - should NOT close
123
+ const dialogBounds = await dialogContent.boundingBox()
124
+
125
+ if (dialogBounds) {
126
+ // Right-click to the left of the dialog (on overlay/backdrop)
127
+ await page.mouse.click(dialogBounds.x - 50, dialogBounds.y + 50, {
128
+ button: 'right',
129
+ })
130
+ }
131
+
132
+ // Wait a bit
133
+ await page.waitForTimeout(500)
134
+
135
+ // Modal dialogs prevent right-click dismiss
136
+ await expect(dialogContent).toBeVisible()
137
+
138
+ // Now try left-click - should close
139
+ if (dialogBounds) {
140
+ await page.mouse.click(dialogBounds.x - 50, dialogBounds.y + 50, { button: 'left' })
141
+ }
142
+
143
+ // Wait for dialog to close
144
+ await page.waitForTimeout(500)
145
+
146
+ // Dialog should be closed after left-click
147
+ await expect(dialogContent).not.toBeVisible()
148
+ })
149
+
150
+ test('auto-focuses first element on mount', async ({ page }) => {
151
+ await page.waitForLoadState('networkidle')
152
+
153
+ const trigger = page.getByTestId('modal-dialog-trigger')
154
+ await trigger.click()
155
+
156
+ const dialogContent = page.getByTestId('modal-dialog-content')
157
+ await expect(dialogContent).toBeVisible({ timeout: 5000 })
158
+
159
+ // Wait for auto-focus
160
+ await page.waitForTimeout(300)
161
+
162
+ // First input should be focused
163
+ const firstInput = dialogContent.getByTestId('first-input')
164
+ await expect(firstInput).toBeFocused()
165
+ })
166
+
167
+ test('returns focus to trigger on close', async ({ page }) => {
168
+ // note: test may be flaky if animations don't complete
169
+ // Manual testing shows the focus return works correctly
170
+ await page.waitForLoadState('networkidle')
171
+
172
+ const trigger = page.getByTestId('modal-dialog-trigger')
173
+ await trigger.click()
174
+
175
+ const dialogContent = page.getByTestId('modal-dialog-content')
176
+ await expect(dialogContent).toBeVisible({ timeout: 5000 })
177
+
178
+ // Wait for animations to complete
179
+ await page.waitForTimeout(500)
180
+
181
+ // Click cancel button via JavaScript (since it may be outside viewport in dialog scroll container)
182
+ await page.evaluate(() => {
183
+ const button = document.querySelector(
184
+ '[data-testid="cancel-button"]'
185
+ ) as HTMLElement
186
+ if (button) button.click()
187
+ })
188
+
189
+ // Wait for dialog to close with animation
190
+ await expect(dialogContent).not.toBeVisible({ timeout: 5000 })
191
+
192
+ // Focus should return to trigger
193
+ await expect(trigger).toBeFocused({ timeout: 5000 })
194
+ })
195
+
196
+ test('handles nested dialogs focus correctly', async ({ page }) => {
197
+ await page.waitForLoadState('networkidle')
198
+
199
+ // Open parent dialog
200
+ const parentTrigger = page.getByTestId('parent-dialog-trigger')
201
+ await parentTrigger.click()
202
+
203
+ const parentDialog = page.getByTestId('parent-dialog-content')
204
+ await expect(parentDialog).toBeVisible({ timeout: 5000 })
205
+
206
+ // Open nested dialog
207
+ const nestedTrigger = parentDialog.getByTestId('nested-dialog-trigger')
208
+ await nestedTrigger.click()
209
+
210
+ const nestedDialog = page.getByTestId('nested-dialog-content')
211
+ await expect(nestedDialog).toBeVisible({ timeout: 5000 })
212
+
213
+ // Focus should be trapped in nested dialog
214
+ await page.waitForTimeout(300)
215
+ const nestedInput = nestedDialog.getByTestId('nested-input')
216
+ await expect(nestedInput).toBeFocused()
217
+
218
+ // Tab should stay within nested dialog
219
+ await page.keyboard.press('Tab')
220
+ const nestedButton = nestedDialog.getByTestId('nested-close-button')
221
+ await expect(nestedButton).toBeFocused()
222
+
223
+ await page.keyboard.press('Tab')
224
+ await expect(nestedInput).toBeFocused() // Should loop back
225
+
226
+ // Close nested dialog
227
+ await nestedButton.click()
228
+ await expect(nestedDialog).not.toBeVisible()
229
+
230
+ // Focus should return to nested trigger in parent dialog
231
+ await expect(nestedTrigger).toBeFocused()
232
+
233
+ // Close parent dialog
234
+ const parentClose = parentDialog.getByTestId('parent-close-button')
235
+ await parentClose.click()
236
+ await expect(parentDialog).not.toBeVisible()
237
+
238
+ // Focus should return to parent trigger
239
+ await expect(parentTrigger).toBeFocused()
240
+ })
241
+
242
+ test('nested dialog appears above parent dialog (z-index stacking)', async ({
243
+ page,
244
+ }) => {
245
+ await page.waitForLoadState('networkidle')
246
+
247
+ // Open parent dialog
248
+ const parentTrigger = page.getByTestId('parent-dialog-trigger')
249
+ await parentTrigger.click()
250
+
251
+ const parentDialog = page.getByTestId('parent-dialog-content')
252
+ await expect(parentDialog).toBeVisible({ timeout: 5000 })
253
+
254
+ // Open nested dialog
255
+ const nestedTrigger = parentDialog.getByTestId('nested-dialog-trigger')
256
+ await nestedTrigger.click()
257
+
258
+ const nestedDialog = page.getByTestId('nested-dialog-content')
259
+ await expect(nestedDialog).toBeVisible({ timeout: 5000 })
260
+
261
+ // Wait for animations to complete
262
+ await page.waitForTimeout(500)
263
+
264
+ // Get the z-index values of both dialog portal containers
265
+ // GuiRoot wraps portal content in Theme > span, so z-index is on the inner span
266
+ const zIndexInfo = await page.evaluate(() => {
267
+ const portals = document.querySelectorAll('span[style*="z-index"]')
268
+ const zIndices: number[] = []
269
+
270
+ portals.forEach((portal) => {
271
+ const style = window.getComputedStyle(portal)
272
+ const zIndex = parseInt(style.zIndex, 10)
273
+ if (!isNaN(zIndex)) {
274
+ zIndices.push(zIndex)
275
+ }
276
+ })
277
+
278
+ return zIndices.sort((a, b) => a - b)
279
+ })
280
+
281
+ // Should have at least 2 different z-index values
282
+ expect(zIndexInfo.length).toBeGreaterThanOrEqual(2)
283
+
284
+ // The nested dialog should have a higher z-index than the parent
285
+ // The last z-index should be higher than the first
286
+ const parentZIndex = zIndexInfo[0]
287
+ const nestedZIndex = zIndexInfo[zIndexInfo.length - 1]
288
+
289
+ expect(nestedZIndex).toBeGreaterThan(parentZIndex)
290
+
291
+ // Additionally verify that the nested dialog is visually on top by checking
292
+ // what element is at the center of the nested dialog
293
+ const nestedBounds = await nestedDialog.boundingBox()
294
+ if (nestedBounds) {
295
+ const centerX = nestedBounds.x + nestedBounds.width / 2
296
+ const centerY = nestedBounds.y + nestedBounds.height / 2
297
+
298
+ const elementAtPoint = await page.evaluate(
299
+ ({ x, y }) => {
300
+ const el = document.elementFromPoint(x, y)
301
+ return el?.closest('[data-testid="nested-dialog-content"]') !== null
302
+ },
303
+ { x: centerX, y: centerY }
304
+ )
305
+
306
+ expect(elementAtPoint).toBe(true)
307
+ }
308
+ })
309
+ })