@wordpress/ui 0.6.1-next.v.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/AGENTS.md +9 -0
  2. package/CHANGELOG.md +32 -1
  3. package/CLAUDE.md +1 -0
  4. package/README.md +13 -12
  5. package/build/badge/badge.cjs +37 -62
  6. package/build/badge/badge.cjs.map +4 -4
  7. package/build/button/button.cjs +3 -3
  8. package/build/button/button.cjs.map +2 -2
  9. package/build/dialog/action.cjs +46 -0
  10. package/build/dialog/action.cjs.map +7 -0
  11. package/build/dialog/close-icon.cjs +57 -0
  12. package/build/dialog/close-icon.cjs.map +7 -0
  13. package/build/dialog/context.cjs +76 -0
  14. package/build/dialog/context.cjs.map +7 -0
  15. package/build/dialog/footer.cjs +64 -0
  16. package/build/dialog/footer.cjs.map +7 -0
  17. package/build/dialog/header.cjs +64 -0
  18. package/build/dialog/header.cjs.map +7 -0
  19. package/build/dialog/index.cjs +52 -0
  20. package/build/dialog/index.cjs.map +7 -0
  21. package/build/dialog/popup.cjs +77 -0
  22. package/build/dialog/popup.cjs.map +7 -0
  23. package/build/dialog/root.cjs +35 -0
  24. package/build/dialog/root.cjs.map +7 -0
  25. package/build/dialog/title.cjs +76 -0
  26. package/build/dialog/title.cjs.map +7 -0
  27. package/build/dialog/trigger.cjs +38 -0
  28. package/build/dialog/trigger.cjs.map +7 -0
  29. package/build/dialog/types.cjs +19 -0
  30. package/build/dialog/types.cjs.map +7 -0
  31. package/build/form/primitives/field/root.cjs +1 -1
  32. package/build/form/primitives/field/root.cjs.map +1 -1
  33. package/build/form/primitives/fieldset/root.cjs +3 -3
  34. package/build/form/primitives/fieldset/root.cjs.map +2 -2
  35. package/build/form/primitives/index.cjs +5 -2
  36. package/build/form/primitives/index.cjs.map +2 -2
  37. package/build/form/primitives/input-layout/input-layout.cjs +3 -3
  38. package/build/form/primitives/input-layout/input-layout.cjs.map +2 -2
  39. package/build/form/primitives/input-layout/slot.cjs +3 -3
  40. package/build/form/primitives/input-layout/slot.cjs.map +2 -2
  41. package/build/form/primitives/select/item.cjs +3 -3
  42. package/build/form/primitives/select/item.cjs.map +2 -2
  43. package/build/form/primitives/select/popup.cjs +3 -3
  44. package/build/form/primitives/select/popup.cjs.map +2 -2
  45. package/build/form/primitives/select/trigger.cjs +3 -3
  46. package/build/form/primitives/select/trigger.cjs.map +2 -2
  47. package/build/{box → form/primitives/textarea}/index.cjs +7 -7
  48. package/build/form/primitives/textarea/index.cjs.map +7 -0
  49. package/build/form/primitives/textarea/textarea.cjs +90 -0
  50. package/build/form/primitives/textarea/textarea.cjs.map +7 -0
  51. package/build/form/primitives/textarea/types.cjs +19 -0
  52. package/build/form/primitives/textarea/types.cjs.map +7 -0
  53. package/build/icon-button/icon-button.cjs +104 -0
  54. package/build/icon-button/icon-button.cjs.map +7 -0
  55. package/build/icon-button/index.cjs +31 -0
  56. package/build/icon-button/index.cjs.map +7 -0
  57. package/build/icon-button/types.cjs +19 -0
  58. package/build/icon-button/types.cjs.map +7 -0
  59. package/build/index.cjs +8 -2
  60. package/build/index.cjs.map +2 -2
  61. package/build/tabs/index.cjs +40 -0
  62. package/build/tabs/index.cjs.map +7 -0
  63. package/build/tabs/list.cjs +145 -0
  64. package/build/tabs/list.cjs.map +7 -0
  65. package/build/tabs/panel.cjs +67 -0
  66. package/build/tabs/panel.cjs.map +7 -0
  67. package/build/tabs/root.cjs +38 -0
  68. package/build/tabs/root.cjs.map +7 -0
  69. package/build/tabs/tab.cjs +71 -0
  70. package/build/tabs/tab.cjs.map +7 -0
  71. package/build/{box → tabs}/types.cjs +1 -1
  72. package/build/tabs/types.cjs.map +7 -0
  73. package/build/tooltip/popup.cjs +3 -3
  74. package/build/tooltip/popup.cjs.map +2 -2
  75. package/build-module/badge/badge.mjs +27 -62
  76. package/build-module/badge/badge.mjs.map +3 -3
  77. package/build-module/button/button.mjs +3 -3
  78. package/build-module/button/button.mjs.map +2 -2
  79. package/build-module/dialog/action.mjs +21 -0
  80. package/build-module/dialog/action.mjs.map +7 -0
  81. package/build-module/dialog/close-icon.mjs +32 -0
  82. package/build-module/dialog/close-icon.mjs.map +7 -0
  83. package/build-module/dialog/context.mjs +57 -0
  84. package/build-module/dialog/context.mjs.map +7 -0
  85. package/build-module/dialog/footer.mjs +29 -0
  86. package/build-module/dialog/footer.mjs.map +7 -0
  87. package/build-module/dialog/header.mjs +29 -0
  88. package/build-module/dialog/header.mjs.map +7 -0
  89. package/build-module/dialog/index.mjs +20 -0
  90. package/build-module/dialog/index.mjs.map +7 -0
  91. package/build-module/dialog/popup.mjs +44 -0
  92. package/build-module/dialog/popup.mjs.map +7 -0
  93. package/build-module/dialog/root.mjs +10 -0
  94. package/build-module/dialog/root.mjs.map +7 -0
  95. package/build-module/dialog/title.mjs +41 -0
  96. package/build-module/dialog/title.mjs.map +7 -0
  97. package/build-module/dialog/trigger.mjs +13 -0
  98. package/build-module/dialog/trigger.mjs.map +7 -0
  99. package/build-module/form/primitives/field/root.mjs +1 -1
  100. package/build-module/form/primitives/field/root.mjs.map +1 -1
  101. package/build-module/form/primitives/fieldset/root.mjs +3 -3
  102. package/build-module/form/primitives/fieldset/root.mjs.map +2 -2
  103. package/build-module/form/primitives/index.mjs +3 -1
  104. package/build-module/form/primitives/index.mjs.map +2 -2
  105. package/build-module/form/primitives/input-layout/input-layout.mjs +3 -3
  106. package/build-module/form/primitives/input-layout/input-layout.mjs.map +2 -2
  107. package/build-module/form/primitives/input-layout/slot.mjs +3 -3
  108. package/build-module/form/primitives/input-layout/slot.mjs.map +2 -2
  109. package/build-module/form/primitives/select/item.mjs +3 -3
  110. package/build-module/form/primitives/select/item.mjs.map +2 -2
  111. package/build-module/form/primitives/select/popup.mjs +3 -3
  112. package/build-module/form/primitives/select/popup.mjs.map +2 -2
  113. package/build-module/form/primitives/select/trigger.mjs +3 -3
  114. package/build-module/form/primitives/select/trigger.mjs.map +2 -2
  115. package/build-module/form/primitives/textarea/index.mjs +6 -0
  116. package/build-module/form/primitives/textarea/index.mjs.map +7 -0
  117. package/build-module/form/primitives/textarea/textarea.mjs +55 -0
  118. package/build-module/form/primitives/textarea/textarea.mjs.map +7 -0
  119. package/build-module/form/primitives/textarea/types.mjs +1 -0
  120. package/build-module/form/primitives/textarea/types.mjs.map +7 -0
  121. package/build-module/icon-button/icon-button.mjs +69 -0
  122. package/build-module/icon-button/icon-button.mjs.map +7 -0
  123. package/build-module/icon-button/index.mjs +6 -0
  124. package/build-module/icon-button/index.mjs.map +7 -0
  125. package/build-module/icon-button/types.mjs +1 -0
  126. package/build-module/icon-button/types.mjs.map +7 -0
  127. package/build-module/index.mjs +5 -1
  128. package/build-module/index.mjs.map +2 -2
  129. package/build-module/tabs/index.mjs +12 -0
  130. package/build-module/tabs/index.mjs.map +7 -0
  131. package/build-module/tabs/list.mjs +110 -0
  132. package/build-module/tabs/list.mjs.map +7 -0
  133. package/build-module/tabs/panel.mjs +32 -0
  134. package/build-module/tabs/panel.mjs.map +7 -0
  135. package/build-module/tabs/root.mjs +13 -0
  136. package/build-module/tabs/root.mjs.map +7 -0
  137. package/build-module/tabs/tab.mjs +36 -0
  138. package/build-module/tabs/tab.mjs.map +7 -0
  139. package/build-module/tabs/types.mjs +1 -0
  140. package/build-module/tabs/types.mjs.map +7 -0
  141. package/build-module/tooltip/popup.mjs +3 -3
  142. package/build-module/tooltip/popup.mjs.map +2 -2
  143. package/build-types/badge/badge.d.ts +1 -2
  144. package/build-types/badge/badge.d.ts.map +1 -1
  145. package/build-types/button/stories/index.story.d.ts +1 -2
  146. package/build-types/button/stories/index.story.d.ts.map +1 -1
  147. package/build-types/dialog/action.d.ts +8 -0
  148. package/build-types/dialog/action.d.ts.map +1 -0
  149. package/build-types/dialog/close-icon.d.ts +8 -0
  150. package/build-types/dialog/close-icon.d.ts.map +1 -0
  151. package/build-types/dialog/context.d.ts +25 -0
  152. package/build-types/dialog/context.d.ts.map +1 -0
  153. package/build-types/dialog/footer.d.ts +8 -0
  154. package/build-types/dialog/footer.d.ts.map +1 -0
  155. package/build-types/dialog/header.d.ts +8 -0
  156. package/build-types/dialog/header.d.ts.map +1 -0
  157. package/build-types/dialog/index.d.ts +10 -0
  158. package/build-types/dialog/index.d.ts.map +1 -0
  159. package/build-types/dialog/popup.d.ts +8 -0
  160. package/build-types/dialog/popup.d.ts.map +1 -0
  161. package/build-types/dialog/root.d.ts +10 -0
  162. package/build-types/dialog/root.d.ts.map +1 -0
  163. package/build-types/dialog/stories/index.story.d.ts +18 -0
  164. package/build-types/dialog/stories/index.story.d.ts.map +1 -0
  165. package/build-types/dialog/test/index.test.d.ts +2 -0
  166. package/build-types/dialog/test/index.test.d.ts.map +1 -0
  167. package/build-types/dialog/title.d.ts +12 -0
  168. package/build-types/dialog/title.d.ts.map +1 -0
  169. package/build-types/dialog/trigger.d.ts +7 -0
  170. package/build-types/dialog/trigger.d.ts.map +1 -0
  171. package/build-types/dialog/types.d.ts +77 -0
  172. package/build-types/dialog/types.d.ts.map +1 -0
  173. package/build-types/form/primitives/field/stories/index.story.d.ts +0 -1
  174. package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
  175. package/build-types/form/primitives/index.d.ts +1 -0
  176. package/build-types/form/primitives/index.d.ts.map +1 -1
  177. package/build-types/form/primitives/input/input.d.ts +1 -1
  178. package/build-types/form/primitives/select/stories/index.story.d.ts +0 -1
  179. package/build-types/form/primitives/select/stories/index.story.d.ts.map +1 -1
  180. package/build-types/form/primitives/textarea/index.d.ts +2 -0
  181. package/build-types/form/primitives/textarea/index.d.ts.map +1 -0
  182. package/build-types/form/primitives/textarea/stories/index.story.d.ts +13 -0
  183. package/build-types/form/primitives/textarea/stories/index.story.d.ts.map +1 -0
  184. package/build-types/form/primitives/textarea/test/index.test.d.ts +2 -0
  185. package/build-types/form/primitives/textarea/test/index.test.d.ts.map +1 -0
  186. package/build-types/form/primitives/textarea/textarea.d.ts +4 -0
  187. package/build-types/form/primitives/textarea/textarea.d.ts.map +1 -0
  188. package/build-types/form/primitives/textarea/types.d.ts +11 -0
  189. package/build-types/form/primitives/textarea/types.d.ts.map +1 -0
  190. package/build-types/icon-button/icon-button.d.ts +13 -0
  191. package/build-types/icon-button/icon-button.d.ts.map +1 -0
  192. package/build-types/icon-button/index.d.ts +2 -0
  193. package/build-types/icon-button/index.d.ts.map +1 -0
  194. package/build-types/icon-button/stories/index.story.d.ts +19 -0
  195. package/build-types/icon-button/stories/index.story.d.ts.map +1 -0
  196. package/build-types/icon-button/test/index.test.d.ts +2 -0
  197. package/build-types/icon-button/test/index.test.d.ts.map +1 -0
  198. package/build-types/icon-button/types.d.ts +36 -0
  199. package/build-types/icon-button/types.d.ts.map +1 -0
  200. package/build-types/index.d.ts +3 -1
  201. package/build-types/index.d.ts.map +1 -1
  202. package/build-types/stack/stories/index.story.d.ts.map +1 -1
  203. package/build-types/tabs/index.d.ts +6 -0
  204. package/build-types/tabs/index.d.ts.map +1 -0
  205. package/build-types/tabs/list.d.ts +16 -0
  206. package/build-types/tabs/list.d.ts.map +1 -0
  207. package/build-types/tabs/panel.d.ts +15 -0
  208. package/build-types/tabs/panel.d.ts.map +1 -0
  209. package/build-types/tabs/root.d.ts +15 -0
  210. package/build-types/tabs/root.d.ts.map +1 -0
  211. package/build-types/tabs/stories/index.story.d.ts +13 -0
  212. package/build-types/tabs/stories/index.story.d.ts.map +1 -0
  213. package/build-types/tabs/tab.d.ts +15 -0
  214. package/build-types/tabs/tab.d.ts.map +1 -0
  215. package/build-types/tabs/test/index.test.d.ts +2 -0
  216. package/build-types/tabs/test/index.test.d.ts.map +1 -0
  217. package/build-types/tabs/types.d.ts +33 -0
  218. package/build-types/tabs/types.d.ts.map +1 -0
  219. package/package.json +12 -10
  220. package/src/badge/badge.tsx +19 -78
  221. package/src/badge/stories/choosing-intent.story.tsx +1 -1
  222. package/src/badge/style.module.css +48 -0
  223. package/src/button/stories/index.story.tsx +3 -16
  224. package/src/button/style.module.css +23 -12
  225. package/src/dialog/action.tsx +22 -0
  226. package/src/dialog/close-icon.tsx +32 -0
  227. package/src/dialog/context.tsx +113 -0
  228. package/src/dialog/footer.tsx +26 -0
  229. package/src/dialog/header.tsx +26 -0
  230. package/src/dialog/index.ts +10 -0
  231. package/src/dialog/popup.tsx +46 -0
  232. package/src/dialog/root.tsx +14 -0
  233. package/src/dialog/stories/index.story.tsx +177 -0
  234. package/src/dialog/style.module.css +114 -0
  235. package/src/dialog/test/index.test.tsx +309 -0
  236. package/src/dialog/title.tsx +39 -0
  237. package/src/dialog/trigger.tsx +14 -0
  238. package/src/dialog/types.ts +93 -0
  239. package/src/form/primitives/field/root.tsx +1 -1
  240. package/src/form/primitives/field/stories/index.story.tsx +0 -1
  241. package/src/form/primitives/fieldset/style.module.css +1 -1
  242. package/src/form/primitives/index.ts +1 -0
  243. package/src/form/primitives/input-layout/style.module.css +5 -8
  244. package/src/form/primitives/select/stories/index.story.tsx +0 -1
  245. package/src/form/primitives/select/test/index.test.tsx +0 -2
  246. package/src/form/primitives/textarea/index.ts +1 -0
  247. package/src/form/primitives/textarea/stories/index.story.tsx +40 -0
  248. package/src/form/primitives/textarea/style.module.css +22 -0
  249. package/src/form/primitives/textarea/test/index.test.tsx +143 -0
  250. package/src/form/primitives/textarea/textarea.tsx +51 -0
  251. package/src/form/primitives/textarea/types.ts +18 -0
  252. package/src/icon-button/icon-button.tsx +65 -0
  253. package/src/icon-button/index.ts +1 -0
  254. package/src/icon-button/stories/index.story.tsx +128 -0
  255. package/src/icon-button/style.module.css +16 -0
  256. package/src/icon-button/test/index.test.tsx +86 -0
  257. package/src/icon-button/types.ts +38 -0
  258. package/src/index.ts +3 -1
  259. package/src/stack/stories/index.story.tsx +4 -5
  260. package/src/tabs/index.ts +6 -0
  261. package/src/tabs/list.tsx +130 -0
  262. package/src/tabs/panel.tsx +23 -0
  263. package/src/tabs/root.tsx +15 -0
  264. package/src/tabs/stories/best-practices.mdx +85 -0
  265. package/src/tabs/stories/index.story.tsx +363 -0
  266. package/src/tabs/style.module.css +269 -0
  267. package/src/tabs/tab.tsx +29 -0
  268. package/src/tabs/test/index.test.tsx +2260 -0
  269. package/src/tabs/types.ts +36 -0
  270. package/src/tooltip/style.module.css +3 -3
  271. package/src/utils/css/item-popup.module.css +2 -2
  272. package/src/utils/css/select-trigger.module.css +1 -1
  273. package/build/box/box.cjs +0 -88
  274. package/build/box/box.cjs.map +0 -7
  275. package/build/box/index.cjs.map +0 -7
  276. package/build/box/types.cjs.map +0 -7
  277. package/build-module/box/box.mjs +0 -63
  278. package/build-module/box/box.mjs.map +0 -7
  279. package/build-module/box/index.mjs +0 -6
  280. package/build-module/box/index.mjs.map +0 -7
  281. package/build-types/box/box.d.ts +0 -7
  282. package/build-types/box/box.d.ts.map +0 -1
  283. package/build-types/box/index.d.ts +0 -2
  284. package/build-types/box/index.d.ts.map +0 -1
  285. package/build-types/box/stories/index.story.d.ts +0 -8
  286. package/build-types/box/stories/index.story.d.ts.map +0 -1
  287. package/build-types/box/test/box.test.d.ts +0 -2
  288. package/build-types/box/test/box.test.d.ts.map +0 -1
  289. package/build-types/box/types.d.ts +0 -46
  290. package/build-types/box/types.d.ts.map +0 -1
  291. package/src/box/box.tsx +0 -118
  292. package/src/box/index.ts +0 -1
  293. package/src/box/stories/index.story.tsx +0 -41
  294. package/src/box/test/box.test.tsx +0 -29
  295. package/src/box/types.ts +0 -61
  296. /package/build-module/{box → dialog}/types.mjs +0 -0
  297. /package/build-module/{box → dialog}/types.mjs.map +0 -0
@@ -0,0 +1,309 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { Component, createRef } from '@wordpress/element';
4
+ import type { ReactNode } from 'react';
5
+ import * as Dialog from '../index';
6
+
7
+ class TestErrorBoundary extends Component<
8
+ { children: ReactNode; onError: ( error: Error ) => void },
9
+ { hasError: boolean }
10
+ > {
11
+ constructor( props: {
12
+ children: ReactNode;
13
+ onError: ( error: Error ) => void;
14
+ } ) {
15
+ super( props );
16
+ this.state = { hasError: false };
17
+ }
18
+
19
+ static getDerivedStateFromError() {
20
+ return { hasError: true };
21
+ }
22
+
23
+ componentDidCatch( error: Error ) {
24
+ this.props.onError( error );
25
+ }
26
+
27
+ render() {
28
+ if ( this.state.hasError ) {
29
+ return null;
30
+ }
31
+
32
+ return this.props.children;
33
+ }
34
+ }
35
+
36
+ describe( 'Dialog', () => {
37
+ it( 'forwards ref', async () => {
38
+ const user = userEvent.setup();
39
+ const triggerRef = createRef< HTMLButtonElement >();
40
+ const popupRef = createRef< HTMLDivElement >();
41
+ const actionRef = createRef< HTMLButtonElement >();
42
+ const headerRef = createRef< HTMLDivElement >();
43
+ const titleRef = createRef< HTMLHeadingElement >();
44
+ const closeIconRef = createRef< HTMLButtonElement >();
45
+ const footerRef = createRef< HTMLDivElement >();
46
+
47
+ render(
48
+ <Dialog.Root>
49
+ <Dialog.Trigger ref={ triggerRef }>Open Dialog</Dialog.Trigger>
50
+ <Dialog.Popup ref={ popupRef }>
51
+ <Dialog.Header ref={ headerRef }>
52
+ <Dialog.Title ref={ titleRef }>
53
+ Test Dialog
54
+ </Dialog.Title>
55
+ <Dialog.CloseIcon ref={ closeIconRef } />
56
+ </Dialog.Header>
57
+ <Dialog.Footer ref={ footerRef }>
58
+ <Dialog.Action ref={ actionRef }>Close</Dialog.Action>
59
+ </Dialog.Footer>
60
+ </Dialog.Popup>
61
+ </Dialog.Root>
62
+ );
63
+
64
+ // Test trigger ref before interaction
65
+ expect( triggerRef.current ).toBeInstanceOf( HTMLButtonElement );
66
+
67
+ // Click trigger to open dialog
68
+ await user.click( triggerRef.current! );
69
+
70
+ // Wait for the dialog to appear
71
+ await waitFor( () => {
72
+ expect( popupRef.current ).toBeInstanceOf( HTMLDivElement );
73
+ } );
74
+
75
+ // Now that the dialog is open, verify all inner refs
76
+ expect( headerRef.current ).toBeInstanceOf( HTMLDivElement );
77
+ expect( titleRef.current ).toBeInstanceOf( HTMLHeadingElement );
78
+ expect( closeIconRef.current ).toBeInstanceOf( HTMLButtonElement );
79
+ expect( actionRef.current ).toBeInstanceOf( HTMLButtonElement );
80
+ expect( footerRef.current ).toBeInstanceOf( HTMLDivElement );
81
+ } );
82
+
83
+ describe( 'Development mode validation', () => {
84
+ // Suppress React's error boundary logging for these tests.
85
+ let originalConsoleError: typeof console.error;
86
+
87
+ beforeEach( () => {
88
+ // eslint-disable-next-line no-console
89
+ originalConsoleError = console.error;
90
+ // eslint-disable-next-line no-console
91
+ console.error = jest.fn();
92
+ } );
93
+
94
+ afterEach( () => {
95
+ // eslint-disable-next-line no-console
96
+ console.error = originalConsoleError;
97
+ } );
98
+
99
+ it( 'should throw when Dialog.Title is missing', async () => {
100
+ const user = userEvent.setup();
101
+ const onError = jest.fn();
102
+
103
+ render(
104
+ <TestErrorBoundary onError={ onError }>
105
+ <Dialog.Root>
106
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
107
+ <Dialog.Popup>
108
+ <Dialog.Header>
109
+ { /* Missing Dialog.Title */ }
110
+ </Dialog.Header>
111
+ <p>Content without a title</p>
112
+ <Dialog.Footer>
113
+ <Dialog.Action>Close</Dialog.Action>
114
+ </Dialog.Footer>
115
+ </Dialog.Popup>
116
+ </Dialog.Root>
117
+ </TestErrorBoundary>
118
+ );
119
+
120
+ // Open the dialog - this will trigger the error in useEffect
121
+ await user.click(
122
+ screen.getByRole( 'button', { name: 'Open Dialog' } )
123
+ );
124
+
125
+ await waitFor( () => {
126
+ expect( onError ).toHaveBeenCalled();
127
+ } );
128
+
129
+ expect( onError.mock.calls[ 0 ][ 0 ] ).toBeInstanceOf( Error );
130
+ expect( ( onError.mock.calls[ 0 ][ 0 ] as Error ).message ).toBe(
131
+ 'Dialog: Missing <Dialog.Title>. ' +
132
+ 'For accessibility, every dialog requires a title. ' +
133
+ 'If needed, the title can be visually hidden but must not be omitted.'
134
+ );
135
+ } );
136
+
137
+ it( 'should not throw before opening the dialog', async () => {
138
+ const onError = jest.fn();
139
+
140
+ render(
141
+ <TestErrorBoundary onError={ onError }>
142
+ <Dialog.Root>
143
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
144
+ <Dialog.Popup>
145
+ <Dialog.Header>
146
+ <Dialog.Title>My Title</Dialog.Title>
147
+ </Dialog.Header>
148
+ <p>Content with a title</p>
149
+ <Dialog.Footer>
150
+ <Dialog.Action>Close</Dialog.Action>
151
+ </Dialog.Footer>
152
+ </Dialog.Popup>
153
+ </Dialog.Root>
154
+ </TestErrorBoundary>
155
+ );
156
+
157
+ // Check that the dialog itself hasn't been rendered in the DOM.
158
+ await expect( screen.findByRole( 'dialog' ) ).rejects.toThrow();
159
+
160
+ expect( onError ).not.toHaveBeenCalled();
161
+ } );
162
+
163
+ it( 'should not throw when Dialog.Title is present', async () => {
164
+ const user = userEvent.setup();
165
+ const onError = jest.fn();
166
+
167
+ render(
168
+ <TestErrorBoundary onError={ onError }>
169
+ <Dialog.Root>
170
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
171
+ <Dialog.Popup>
172
+ <Dialog.Header>
173
+ <Dialog.Title>My Title</Dialog.Title>
174
+ </Dialog.Header>
175
+ <p>Content with a title</p>
176
+ <Dialog.Footer>
177
+ <Dialog.Action>Close</Dialog.Action>
178
+ </Dialog.Footer>
179
+ </Dialog.Popup>
180
+ </Dialog.Root>
181
+ </TestErrorBoundary>
182
+ );
183
+
184
+ // Open the dialog - should not throw
185
+ await user.click(
186
+ screen.getByRole( 'button', { name: 'Open Dialog' } )
187
+ );
188
+
189
+ // Wait for the dialog to appear and ensure validation does not trigger errors
190
+ await waitFor( () => {
191
+ expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
192
+ } );
193
+ expect( onError ).not.toHaveBeenCalled();
194
+ } );
195
+
196
+ it( 'should throw when Dialog.Title is empty', async () => {
197
+ const user = userEvent.setup();
198
+ const onError = jest.fn();
199
+
200
+ render(
201
+ <TestErrorBoundary onError={ onError }>
202
+ <Dialog.Root>
203
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
204
+ <Dialog.Popup>
205
+ <Dialog.Header>
206
+ { /* @ts-expect-error this is just for test purposes */ }
207
+ <Dialog.Title>
208
+ { /* Empty title */ }
209
+ </Dialog.Title>
210
+ </Dialog.Header>
211
+ <p>Content with empty title</p>
212
+ <Dialog.Footer>
213
+ <Dialog.Action>Close</Dialog.Action>
214
+ </Dialog.Footer>
215
+ </Dialog.Popup>
216
+ </Dialog.Root>
217
+ </TestErrorBoundary>
218
+ );
219
+
220
+ // Open the dialog - this will trigger the error
221
+ await user.click(
222
+ screen.getByRole( 'button', { name: 'Open Dialog' } )
223
+ );
224
+
225
+ await waitFor( () => {
226
+ expect( onError ).toHaveBeenCalled();
227
+ } );
228
+
229
+ expect( onError.mock.calls[ 0 ][ 0 ] ).toBeInstanceOf( Error );
230
+ expect( ( onError.mock.calls[ 0 ][ 0 ] as Error ).message ).toBe(
231
+ 'Dialog: <Dialog.Title> cannot be empty. ' +
232
+ 'Provide meaningful text content for the dialog title.'
233
+ );
234
+ } );
235
+
236
+ it( 'should throw when Dialog.Title contains only whitespace', async () => {
237
+ const user = userEvent.setup();
238
+ const onError = jest.fn();
239
+
240
+ render(
241
+ <TestErrorBoundary onError={ onError }>
242
+ <Dialog.Root>
243
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
244
+ <Dialog.Popup>
245
+ <Dialog.Header>
246
+ <Dialog.Title> </Dialog.Title>
247
+ </Dialog.Header>
248
+ <p>Content with whitespace-only title</p>
249
+ <Dialog.Footer>
250
+ <Dialog.Action>Close</Dialog.Action>
251
+ </Dialog.Footer>
252
+ </Dialog.Popup>
253
+ </Dialog.Root>
254
+ </TestErrorBoundary>
255
+ );
256
+
257
+ // Open the dialog - this will trigger the error
258
+ await user.click(
259
+ screen.getByRole( 'button', { name: 'Open Dialog' } )
260
+ );
261
+
262
+ await waitFor( () => {
263
+ expect( onError ).toHaveBeenCalled();
264
+ } );
265
+
266
+ expect( onError.mock.calls[ 0 ][ 0 ] ).toBeInstanceOf( Error );
267
+ expect( ( onError.mock.calls[ 0 ][ 0 ] as Error ).message ).toBe(
268
+ 'Dialog: <Dialog.Title> cannot be empty. ' +
269
+ 'Provide meaningful text content for the dialog title.'
270
+ );
271
+ } );
272
+
273
+ it( 'should not throw when Dialog.Title contains mixed content with text', async () => {
274
+ const user = userEvent.setup();
275
+ const onError = jest.fn();
276
+
277
+ render(
278
+ <TestErrorBoundary onError={ onError }>
279
+ <Dialog.Root>
280
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
281
+ <Dialog.Popup>
282
+ <Dialog.Header>
283
+ <Dialog.Title>
284
+ <span aria-hidden="true">🎉</span>
285
+ Settings
286
+ </Dialog.Title>
287
+ </Dialog.Header>
288
+ <p>Content with icon and text title</p>
289
+ <Dialog.Footer>
290
+ <Dialog.Action>Close</Dialog.Action>
291
+ </Dialog.Footer>
292
+ </Dialog.Popup>
293
+ </Dialog.Root>
294
+ </TestErrorBoundary>
295
+ );
296
+
297
+ // Open the dialog - should not throw
298
+ await user.click(
299
+ screen.getByRole( 'button', { name: 'Open Dialog' } )
300
+ );
301
+
302
+ // Wait for the dialog to appear and ensure validation does not trigger errors
303
+ await waitFor( () => {
304
+ expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
305
+ } );
306
+ expect( onError ).not.toHaveBeenCalled();
307
+ } );
308
+ } );
309
+ } );
@@ -0,0 +1,39 @@
1
+ import { Dialog as _Dialog } from '@base-ui/react/dialog';
2
+ import clsx from 'clsx';
3
+ import { useMergeRefs } from '@wordpress/compose';
4
+ import { forwardRef, useLayoutEffect, useRef } from '@wordpress/element';
5
+ import { useDialogValidationContext } from './context';
6
+ import styles from './style.module.css';
7
+ import type { TitleProps } from './types';
8
+
9
+ /**
10
+ * Renders the dialog title. This component is required for accessibility
11
+ * and serves as both the visible heading and the accessible label for
12
+ * the dialog.
13
+ *
14
+ * Base UI's Dialog.Title renders an `<h2>` by default. Use the `render` prop
15
+ * to customize the element if needed.
16
+ */
17
+ const Title = forwardRef< HTMLHeadingElement, TitleProps >(
18
+ function DialogTitle( { className, render, ...props }, forwardedRef ) {
19
+ const validationContext = useDialogValidationContext();
20
+ const internalRef = useRef< HTMLHeadingElement >( null );
21
+ const mergedRef = useMergeRefs( [ internalRef, forwardedRef ] );
22
+
23
+ // Register this title with the parent Popup for validation (dev only)
24
+ useLayoutEffect( () => {
25
+ validationContext?.registerTitle( internalRef.current );
26
+ }, [ validationContext ] );
27
+
28
+ return (
29
+ <_Dialog.Title
30
+ ref={ mergedRef }
31
+ className={ clsx( styles.title, className ) }
32
+ render={ render }
33
+ { ...props }
34
+ />
35
+ );
36
+ }
37
+ );
38
+
39
+ export { Title };
@@ -0,0 +1,14 @@
1
+ import { Dialog as _Dialog } from '@base-ui/react/dialog';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import type { TriggerProps } from './types';
4
+
5
+ /**
6
+ * Renders a button that opens the dialog popup when clicked.
7
+ */
8
+ const Trigger = forwardRef< HTMLButtonElement, TriggerProps >(
9
+ function DialogTrigger( props, ref ) {
10
+ return <_Dialog.Trigger ref={ ref } { ...props } />;
11
+ }
12
+ );
13
+
14
+ export { Trigger };
@@ -0,0 +1,93 @@
1
+ import type { Dialog as _Dialog } from '@base-ui/react/dialog';
2
+ import type { ReactNode } from 'react';
3
+ import type { Button } from '../button';
4
+ import type { IconButton } from '../icon-button';
5
+ import type { ComponentProps } from '../utils/types';
6
+
7
+ export interface RootProps
8
+ extends Pick<
9
+ _Dialog.Root.Props,
10
+ 'open' | 'onOpenChange' | 'defaultOpen' | 'modal'
11
+ > {
12
+ /**
13
+ * The content to be rendered inside the component.
14
+ */
15
+ children?: ReactNode;
16
+ }
17
+
18
+ export interface TriggerProps extends ComponentProps< 'button' > {
19
+ /**
20
+ * The content to be rendered inside the component.
21
+ */
22
+ children?: ReactNode;
23
+ }
24
+
25
+ export interface PopupProps extends ComponentProps< 'div' > {
26
+ /**
27
+ * The content to be rendered inside the component.
28
+ */
29
+ children?: ReactNode;
30
+
31
+ /**
32
+ * Renders the dialog at a preset width (excluding additional padding from
33
+ * the viewport edges).
34
+ *
35
+ * - `'small'` — max-width of 384px.
36
+ * - `'medium'` — max-width of 512px.
37
+ * - `'large'` — max-width of 840px.
38
+ * - `'stretch'` — no max-width, stretches to fill available space.
39
+ * - `'full'` — stretches to fill available width and height.
40
+ *
41
+ * @default 'medium'
42
+ */
43
+ size?: 'small' | 'medium' | 'large' | 'stretch' | 'full';
44
+ }
45
+
46
+ export interface ActionProps extends ComponentProps< typeof Button > {
47
+ /**
48
+ * The content to be rendered inside the component.
49
+ */
50
+ children?: ReactNode;
51
+ }
52
+
53
+ export interface FooterProps extends ComponentProps< 'div' > {
54
+ /**
55
+ * The content to be rendered inside the component.
56
+ */
57
+ children?: ReactNode;
58
+ }
59
+
60
+ export interface HeaderProps extends ComponentProps< 'div' > {
61
+ /**
62
+ * The content to be rendered inside the component.
63
+ */
64
+ children?: ReactNode;
65
+ }
66
+
67
+ export interface TitleProps extends ComponentProps< 'h2' > {
68
+ /**
69
+ * The title content to be rendered. This serves as both the visible
70
+ * heading and the accessible label for the dialog.
71
+ */
72
+ children: ReactNode;
73
+ }
74
+
75
+ export interface CloseIconProps
76
+ extends Omit<
77
+ ComponentProps< typeof IconButton >,
78
+ 'label' | 'icon' | 'loading' | 'loadingAnnouncement'
79
+ > {
80
+ /**
81
+ * A label describing the button's action, shown as a tooltip and to
82
+ * assistive technology.
83
+ *
84
+ * @default __( 'Close' )
85
+ */
86
+ label?: ComponentProps< typeof IconButton >[ 'label' ];
87
+ /**
88
+ * The icon to display in the button.
89
+ *
90
+ * @default the `close` icon from `@wordpress/icons`
91
+ */
92
+ icon?: ComponentProps< typeof IconButton >[ 'icon' ];
93
+ }
@@ -6,7 +6,7 @@ import type { FieldRootProps } from './types';
6
6
  import { Stack } from '../../../stack';
7
7
 
8
8
  const DEFAULT_RENDER = ( props: React.ComponentProps< typeof Stack > ) => (
9
- <Stack { ...props } direction="column" gap="xs" />
9
+ <Stack { ...props } direction="column" gap="sm" />
10
10
  );
11
11
 
12
12
  /**
@@ -1,6 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { useId } from '@wordpress/element';
3
- import '@wordpress/theme/design-tokens.css';
4
3
  import { Field } from '../../../..';
5
4
 
6
5
  const meta: Meta< typeof Field.Root > = {
@@ -4,7 +4,7 @@
4
4
  .root {
5
5
  display: flex;
6
6
  flex-direction: column;
7
- gap: var(--wpds-dimension-gap-2xs);
7
+ gap: var(--wpds-dimension-gap-xs);
8
8
  border: 0;
9
9
  margin: 0;
10
10
  padding: 0;
@@ -3,3 +3,4 @@ export * as Fieldset from './fieldset';
3
3
  export { Input } from './input';
4
4
  export { InputLayout } from './input-layout';
5
5
  export * as Select from './select';
6
+ export { Textarea } from './textarea';
@@ -2,8 +2,7 @@
2
2
 
3
3
  @layer wp-ui-components {
4
4
  .input-layout {
5
- /* TODO: Use padding tokens */
6
- --wp-ui-input-layout-padding-inline: calc(var(--wpds-dimension-base) * 3);
5
+ --wp-ui-input-layout-padding-inline: var(--wpds-dimension-padding-md);
7
6
 
8
7
  display: flex;
9
8
  height: 40px;
@@ -22,14 +21,12 @@
22
21
  }
23
22
 
24
23
  &.is-size-compact {
25
- /* TODO: Use padding tokens */
26
- --wp-ui-input-layout-padding-inline: calc(var(--wpds-dimension-base) * 2);
24
+ --wp-ui-input-layout-padding-inline: var(--wpds-dimension-padding-sm);
27
25
  height: 32px;
28
26
  }
29
27
 
30
28
  &.is-size-small {
31
- /* TODO: Use padding tokens */
32
- --wp-ui-input-layout-padding-inline: calc(var(--wpds-dimension-base) * 2);
29
+ --wp-ui-input-layout-padding-inline: var(--wpds-dimension-padding-sm);
33
30
  height: 24px;
34
31
  }
35
32
 
@@ -66,10 +63,10 @@
66
63
  &.is-padding-minimal {
67
64
  --wp-ui-input-layout-prefix-padding-start:
68
65
  calc(var(--wp-ui-input-layout-padding-inline) -
69
- var(--wpds-dimension-base));
66
+ var(--wpds-dimension-padding-xs));
70
67
  --wp-ui-input-layout-suffix-padding-end:
71
68
  calc(var(--wp-ui-input-layout-padding-inline) -
72
- var(--wpds-dimension-base));
69
+ var(--wpds-dimension-padding-xs));
73
70
  }
74
71
 
75
72
  &.is-prefix {
@@ -1,5 +1,4 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import '@wordpress/theme/design-tokens.css';
3
2
  import { Select } from '../../../..';
4
3
 
5
4
  const meta: Meta< typeof Select.Root > = {
@@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event';
3
3
  import { createRef } from '@wordpress/element';
4
4
  import * as Select from '../index';
5
5
 
6
- jest.setTimeout( 10000 );
7
-
8
6
  describe( 'Select', () => {
9
7
  it( 'forwards ref', async () => {
10
8
  const user = userEvent.setup();
@@ -0,0 +1 @@
1
+ export { Textarea } from './textarea';
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Textarea } from '../index';
3
+
4
+ const meta: Meta< typeof Textarea > = {
5
+ title: 'Design System/Components/Form/Primitives/Textarea',
6
+ component: Textarea,
7
+ };
8
+ export default meta;
9
+
10
+ type Story = StoryObj< typeof Textarea >;
11
+
12
+ export const Default: Story = {
13
+ args: {
14
+ placeholder: 'Placeholder',
15
+ },
16
+ };
17
+
18
+ export const Disabled: Story = {
19
+ args: {
20
+ ...Default.args,
21
+ disabled: true,
22
+ },
23
+ };
24
+
25
+ export const WithOverflow: Story = {
26
+ args: {
27
+ ...Default.args,
28
+ defaultValue: `Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`,
29
+ },
30
+ };
31
+
32
+ /**
33
+ * When `rows` is set to `1`, the textarea will have the same footprint as a default `Input`.
34
+ */
35
+ export const WithOneRow: Story = {
36
+ args: {
37
+ ...Default.args,
38
+ rows: 1,
39
+ },
40
+ };
@@ -0,0 +1,22 @@
1
+ @layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;
2
+
3
+ @layer wp-ui-components {
4
+ .wrapper {
5
+ --wp-ui-textarea-min-height: 40px;
6
+ }
7
+
8
+ .textarea {
9
+ /* Prevents users from resizing the textarea below this height. */
10
+ min-height: calc(var(--wp-ui-textarea-min-height) - 2px);
11
+ resize: block;
12
+ }
13
+ }
14
+
15
+ @layer wp-ui-compositions {
16
+ .wrapper {
17
+ --wp-ui-input-padding-block: 9.9px;
18
+
19
+ height: auto;
20
+ line-height: 1.4; /* TODO: Use variable */
21
+ }
22
+ }