@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,143 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { createRef, useState } from '@wordpress/element';
4
+ import { Textarea } from '../index';
5
+
6
+ describe( 'Textarea', () => {
7
+ it( 'forwards ref', () => {
8
+ const ref = createRef< HTMLTextAreaElement >();
9
+
10
+ render( <Textarea ref={ ref } /> );
11
+
12
+ expect( ref.current ).toBeInstanceOf( HTMLTextAreaElement );
13
+ } );
14
+
15
+ describe( 'value prop', () => {
16
+ it( 'renders with controlled value', () => {
17
+ render( <Textarea value="Hello, world!" /> );
18
+
19
+ const textarea = screen.getByRole( 'textbox' );
20
+ expect( textarea ).toHaveValue( 'Hello, world!' );
21
+ } );
22
+ } );
23
+
24
+ describe( 'defaultValue prop', () => {
25
+ it( 'renders with default value', () => {
26
+ render( <Textarea defaultValue="Default content" /> );
27
+
28
+ const textarea = screen.getByRole( 'textbox' );
29
+ expect( textarea ).toHaveValue( 'Default content' );
30
+ } );
31
+
32
+ it( 'allows user to modify uncontrolled value', async () => {
33
+ const user = userEvent.setup();
34
+ render( <Textarea defaultValue="Default content" /> );
35
+
36
+ const textarea = screen.getByRole( 'textbox' );
37
+ expect( textarea ).toHaveValue( 'Default content' );
38
+
39
+ // Clear and type new content
40
+ await user.clear( textarea );
41
+ await user.type( textarea, 'New content' );
42
+
43
+ expect( textarea ).toHaveValue( 'New content' );
44
+ } );
45
+ } );
46
+
47
+ describe( 'onValueChange prop', () => {
48
+ it( 'calls onValueChange when user types', async () => {
49
+ const user = userEvent.setup();
50
+ const handleValueChange = jest.fn();
51
+
52
+ render(
53
+ <Textarea defaultValue="" onValueChange={ handleValueChange } />
54
+ );
55
+
56
+ const textarea = screen.getByRole( 'textbox' );
57
+
58
+ await user.type( textarea, 'Hello' );
59
+
60
+ expect( handleValueChange ).toHaveBeenLastCalledWith(
61
+ 'Hello',
62
+ expect.any( Object )
63
+ );
64
+
65
+ await user.clear( textarea );
66
+
67
+ expect( handleValueChange ).toHaveBeenLastCalledWith(
68
+ '',
69
+ expect.any( Object )
70
+ );
71
+ } );
72
+
73
+ it( 'works with controlled component pattern', async () => {
74
+ const user = userEvent.setup();
75
+ const handleValueChange = jest.fn();
76
+
77
+ const ControlledTextarea = () => {
78
+ const [ value, setValue ] = useState( 'Initial' );
79
+
80
+ return (
81
+ <Textarea
82
+ value={ value }
83
+ onValueChange={ ( newValue ) => {
84
+ handleValueChange( newValue );
85
+ setValue( newValue );
86
+ } }
87
+ />
88
+ );
89
+ };
90
+
91
+ render( <ControlledTextarea /> );
92
+
93
+ const textarea = screen.getByRole( 'textbox' );
94
+
95
+ await user.clear( textarea );
96
+ await user.type( textarea, 'Updated' );
97
+
98
+ expect( handleValueChange ).toHaveBeenLastCalledWith( 'Updated' );
99
+ } );
100
+ } );
101
+
102
+ describe( 'render prop', () => {
103
+ it( 'correctly merges props with custom render function', () => {
104
+ render(
105
+ <Textarea
106
+ render={ ( props ) => (
107
+ <div data-testid="my-render" { ...props } />
108
+ ) }
109
+ data-my-attribute
110
+ />
111
+ );
112
+
113
+ expect( screen.getByTestId( 'my-render' ) ).toHaveAttribute(
114
+ 'data-my-attribute'
115
+ );
116
+ } );
117
+
118
+ it( 'correctly merges props with custom render element', () => {
119
+ render(
120
+ <Textarea
121
+ render={ <div data-testid="my-render" /> }
122
+ data-my-attribute
123
+ />
124
+ );
125
+
126
+ expect( screen.getByTestId( 'my-render' ) ).toHaveAttribute(
127
+ 'data-my-attribute'
128
+ );
129
+ } );
130
+ } );
131
+
132
+ it( 'disables the textarea when disabled prop is true', () => {
133
+ render( <Textarea disabled /> );
134
+
135
+ expect( screen.getByRole( 'textbox' ) ).toBeDisabled();
136
+ } );
137
+
138
+ it( 'applies custom rows value', () => {
139
+ render( <Textarea rows={ 10 } /> );
140
+
141
+ expect( screen.getByRole( 'textbox' ) ).toHaveAttribute( 'rows', '10' );
142
+ } );
143
+ } );
@@ -0,0 +1,51 @@
1
+ import { mergeProps } from '@base-ui/react';
2
+ import clsx from 'clsx';
3
+ import { cloneElement, forwardRef } from '@wordpress/element';
4
+ import styles from './style.module.css';
5
+ import type { TextareaProps } from './types';
6
+ import { Input } from '../input';
7
+
8
+ const wrappedRender = (
9
+ render: NonNullable< TextareaProps[ 'render' ] >,
10
+ restProps: TextareaProps & { ref: React.Ref< HTMLTextAreaElement > }
11
+ ) => {
12
+ return function Render(
13
+ props: React.HTMLAttributes< HTMLTextAreaElement >
14
+ ) {
15
+ return typeof render === 'function'
16
+ ? render( mergeProps( props, restProps ) )
17
+ : cloneElement( render, mergeProps( props, restProps ) );
18
+ };
19
+ };
20
+
21
+ export const Textarea = forwardRef< HTMLTextAreaElement, TextareaProps >(
22
+ function Textarea(
23
+ {
24
+ className,
25
+ defaultValue,
26
+ disabled,
27
+ onValueChange,
28
+ render,
29
+ rows = 4,
30
+ style,
31
+ value,
32
+ ...restProps
33
+ },
34
+ ref
35
+ ) {
36
+ return (
37
+ <Input
38
+ className={ clsx( styles.wrapper, className ) }
39
+ style={ style }
40
+ render={ wrappedRender(
41
+ render || ( ( props ) => <textarea { ...props } /> ),
42
+ { className: styles.textarea, ref, rows, ...restProps }
43
+ ) }
44
+ value={ value }
45
+ defaultValue={ defaultValue }
46
+ onValueChange={ onValueChange }
47
+ disabled={ disabled }
48
+ />
49
+ );
50
+ }
51
+ );
@@ -0,0 +1,18 @@
1
+ import type { InputProps } from '../input/types';
2
+ import type { ComponentProps } from '../../../utils/types';
3
+
4
+ export type TextareaProps = Omit<
5
+ ComponentProps< 'textarea' >,
6
+ 'disabled' | 'rows' | 'value' | 'defaultValue'
7
+ > &
8
+ Pick<
9
+ InputProps,
10
+ 'value' | 'defaultValue' | 'onValueChange' | 'disabled'
11
+ > & {
12
+ /**
13
+ * The number of rows the textarea should contain.
14
+ *
15
+ * @default 4
16
+ */
17
+ rows?: React.ComponentProps< 'textarea' >[ 'rows' ];
18
+ };
@@ -0,0 +1,65 @@
1
+ import clsx from 'clsx';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import { Button } from '../button';
4
+ import { Icon } from '../icon';
5
+ import * as Tooltip from '../tooltip';
6
+ import styles from './style.module.css';
7
+ import { type IconButtonProps } from './types';
8
+
9
+ /**
10
+ * An icon-only button with automatic tooltip and optimized styling.
11
+ * Inherits all Button props while providing icon-specific enhancements.
12
+ */
13
+ export const IconButton = forwardRef< HTMLButtonElement, IconButtonProps >(
14
+ function IconButton(
15
+ {
16
+ label,
17
+ className,
18
+ // Prevent accidental forwarding of `children`
19
+ children: _children,
20
+ icon,
21
+ size,
22
+ shortcut,
23
+ ...restProps
24
+ }: IconButtonProps & { children?: unknown },
25
+ ref
26
+ ) {
27
+ const classes = clsx( styles[ 'icon-button' ], className );
28
+
29
+ return (
30
+ <Tooltip.Provider delay={ 0 }>
31
+ <Tooltip.Root>
32
+ <Tooltip.Trigger
33
+ ref={ ref }
34
+ render={
35
+ <Button
36
+ { ...restProps }
37
+ size={ size }
38
+ aria-label={ label }
39
+ aria-keyshortcuts={ shortcut?.ariaKeyShortcut }
40
+ />
41
+ }
42
+ className={ classes }
43
+ >
44
+ <Icon
45
+ icon={ icon }
46
+ size={ 24 }
47
+ className={ styles.icon }
48
+ />
49
+ </Tooltip.Trigger>
50
+ <Tooltip.Popup>
51
+ { label }
52
+ { shortcut && (
53
+ <>
54
+ { ' ' }
55
+ <span aria-hidden="true">
56
+ { shortcut.displayShortcut }
57
+ </span>
58
+ </>
59
+ ) }
60
+ </Tooltip.Popup>
61
+ </Tooltip.Root>
62
+ </Tooltip.Provider>
63
+ );
64
+ }
65
+ );
@@ -0,0 +1 @@
1
+ export { IconButton } from './icon-button';
@@ -0,0 +1,128 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import {
3
+ cog,
4
+ copy,
5
+ download,
6
+ pencil,
7
+ plus,
8
+ trash,
9
+ upload,
10
+ wordpress,
11
+ } from '@wordpress/icons';
12
+ import { displayShortcut, ariaKeyShortcut } from '@wordpress/keycodes';
13
+ import { IconButton } from '../index';
14
+
15
+ const meta: Meta< typeof IconButton > = {
16
+ title: 'Design System/Components/IconButton',
17
+ component: IconButton,
18
+ argTypes: {
19
+ 'aria-pressed': {
20
+ control: { type: 'boolean' },
21
+ },
22
+ },
23
+ };
24
+ export default meta;
25
+
26
+ type Story = StoryObj< typeof IconButton >;
27
+
28
+ export const Default: Story = {
29
+ args: {
30
+ icon: cog,
31
+ label: 'Settings',
32
+ },
33
+ };
34
+
35
+ export const Outline: Story = {
36
+ ...Default,
37
+ args: {
38
+ ...Default.args,
39
+ variant: 'outline',
40
+ },
41
+ };
42
+
43
+ export const Minimal: Story = {
44
+ ...Default,
45
+ args: {
46
+ ...Default.args,
47
+ variant: 'minimal',
48
+ },
49
+ };
50
+
51
+ export const Neutral: Story = {
52
+ ...Default,
53
+ args: {
54
+ ...Default.args,
55
+ tone: 'neutral',
56
+ label: 'Settings',
57
+ },
58
+ };
59
+
60
+ export const NeutralOutline: Story = {
61
+ ...Default,
62
+ args: {
63
+ ...Default.args,
64
+ tone: 'neutral',
65
+ variant: 'outline',
66
+ label: 'Settings',
67
+ },
68
+ };
69
+
70
+ export const Disabled: Story = {
71
+ ...Default,
72
+ args: {
73
+ ...Default.args,
74
+ disabled: true,
75
+ label: 'Settings',
76
+ },
77
+ };
78
+
79
+ export const WithDifferentIcons: Story = {
80
+ ...Default,
81
+ render: ( args ) => (
82
+ <div
83
+ style={ {
84
+ display: 'flex',
85
+ gap: '1rem',
86
+ alignItems: 'center',
87
+ flexWrap: 'wrap',
88
+ } }
89
+ >
90
+ <IconButton { ...args } icon={ wordpress } label="WordPress" />
91
+ <IconButton { ...args } icon={ plus } label="Add" />
92
+ <IconButton { ...args } icon={ pencil } label="Edit" />
93
+ <IconButton { ...args } icon={ trash } label="Delete" />
94
+ <IconButton { ...args } icon={ download } label="Download" />
95
+ <IconButton { ...args } icon={ upload } label="Upload" />
96
+ </div>
97
+ ),
98
+ };
99
+
100
+ /**
101
+ * The pressed state is only available for buttons with `tone="neutral"` and
102
+ * `variant="minimal"` and can be toggled via the `aria-pressed` HTML attribute.
103
+ */
104
+ export const Pressed: Story = {
105
+ ...Default,
106
+ args: {
107
+ ...Default.args,
108
+ tone: 'neutral',
109
+ variant: 'minimal',
110
+ label: 'Toggle Settings',
111
+ 'aria-pressed': true,
112
+ },
113
+ };
114
+
115
+ const EXAMPLE_SHORTCUT_OBJECT = {
116
+ displayShortcut: displayShortcut.primary( 'c' ),
117
+ ariaKeyShortcut: ariaKeyShortcut.primary( 'c' ),
118
+ };
119
+
120
+ export const WithShortcut: Story = {
121
+ ...Default,
122
+ args: {
123
+ ...Default.args,
124
+ icon: copy,
125
+ label: 'Copy',
126
+ shortcut: EXAMPLE_SHORTCUT_OBJECT,
127
+ },
128
+ };
@@ -0,0 +1,16 @@
1
+ @layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;
2
+
3
+ @layer wp-ui-compositions {
4
+ .icon-button {
5
+ --wp-ui-button-aspect-ratio: 1;
6
+ --wp-ui-button-padding-inline: 0;
7
+ --wp-ui-button-min-width: unset;
8
+ }
9
+
10
+ .icon {
11
+ /* Compensate for the button's 1px border so the icon extends
12
+ edge-to-edge and doesn't inflate the button's dimensions
13
+ (e.g. 24px icon inside a 24px-tall small button). */
14
+ margin: -1px;
15
+ }
16
+ }
@@ -0,0 +1,86 @@
1
+ import { render, waitFor, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { createRef } from '@wordpress/element';
4
+ import { IconButton } from '../index';
5
+
6
+ describe( 'IconButton', () => {
7
+ it( 'forwards ref', () => {
8
+ const ref = createRef< HTMLButtonElement >();
9
+
10
+ render( <IconButton ref={ ref } label="Click me" icon={ <svg /> } /> );
11
+
12
+ expect( ref.current ).toBeInstanceOf( HTMLButtonElement );
13
+ } );
14
+
15
+ it( 'respects custom render prop as handled by Button', () => {
16
+ render(
17
+ <IconButton
18
+ label="Click me"
19
+ icon={ <svg /> }
20
+ variant="outline"
21
+ disabled
22
+ focusableWhenDisabled
23
+ render={ <button data-testid="button" /> }
24
+ />
25
+ );
26
+
27
+ // Should render as a button from `render` prop...
28
+ const button = screen.getByRole( 'button', { name: 'Click me' } );
29
+ expect( button ).toHaveAttribute( 'data-testid', 'button' );
30
+
31
+ // ...and still inherit the behavior of Button
32
+ expect( button ).toBeEnabled();
33
+ expect( button ).toHaveAttribute( 'aria-disabled', 'true' );
34
+ } );
35
+
36
+ describe( 'shortcut', () => {
37
+ it( 'sets aria-keyshortcuts attribute on the button', () => {
38
+ const { rerender } = render(
39
+ <IconButton
40
+ label="Save"
41
+ icon={ <svg /> }
42
+ shortcut={ {
43
+ displayShortcut: '⌘S',
44
+ ariaKeyShortcut: 'Meta+S',
45
+ } }
46
+ />
47
+ );
48
+
49
+ const button = screen.getByRole( 'button', { name: 'Save' } );
50
+ expect( button ).toHaveAttribute( 'aria-keyshortcuts', 'Meta+S' );
51
+
52
+ // The aria-keyshortcuts attribute is removed when there is no
53
+ // `shortcut` prop.
54
+ rerender( <IconButton label="Save" icon={ <svg /> } /> );
55
+ expect( button ).not.toHaveAttribute( 'aria-keyshortcuts' );
56
+ } );
57
+
58
+ it( 'displays the shortcut in the tooltip but hides it from assistive technology', async () => {
59
+ const user = userEvent.setup();
60
+
61
+ render(
62
+ <IconButton
63
+ label="Save"
64
+ icon={ <svg /> }
65
+ shortcut={ {
66
+ displayShortcut: '⌘S',
67
+ ariaKeyShortcut: 'Meta+S',
68
+ } }
69
+ />
70
+ );
71
+
72
+ const button = screen.getByRole( 'button', { name: 'Save' } );
73
+ await user.hover( button );
74
+
75
+ await waitFor( () => {
76
+ const shortcutElement = screen.getByText( '⌘S' );
77
+ expect( shortcutElement ).toBeVisible();
78
+ } );
79
+
80
+ expect( screen.getByText( '⌘S' ) ).toHaveAttribute(
81
+ 'aria-hidden',
82
+ 'true'
83
+ );
84
+ } );
85
+ } );
86
+ } );
@@ -0,0 +1,38 @@
1
+ import { type ButtonProps } from '../button/types';
2
+ import { type IconProps } from '../icon/types';
3
+
4
+ export type IconButtonProps = Omit< ButtonProps, 'children' > & {
5
+ /**
6
+ * A label describing the button's action, shown as a tooltip and to
7
+ * assistive technology.
8
+ */
9
+ label: string;
10
+
11
+ /**
12
+ * The icon to display in the button.
13
+ */
14
+ icon: IconProps[ 'icon' ];
15
+
16
+ /**
17
+ * The keyboard shortcut associated with this button. When provided, the
18
+ * shortcut is displayed in the tooltip and announced to assistive technology.
19
+ *
20
+ * **Note**: This prop is for display and accessibility purposes only — the
21
+ * consumer is responsible for implementing the actual keyboard event handler.
22
+ */
23
+ shortcut?: {
24
+ /**
25
+ * The human-readable representation of the shortcut, displayed in the
26
+ * tooltip. Use platform-appropriate symbols (e.g., "⌘S" on macOS,
27
+ * "Ctrl+S" on Windows).
28
+ */
29
+ displayShortcut: string;
30
+ /**
31
+ * The shortcut in a format compatible with the
32
+ * [aria-keyshortcuts](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-keyshortcuts)
33
+ * attribute. Use "+" to separate keys and standard key names
34
+ * (e.g., "Meta+S", "Control+Shift+P").
35
+ */
36
+ ariaKeyShortcut: string;
37
+ };
38
+ };
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export * from './badge';
2
- export * from './box';
3
2
  export * from './button';
3
+ export * as Dialog from './dialog';
4
4
  export * from './form/primitives';
5
5
  export * from './icon';
6
+ export * from './icon-button';
6
7
  export * from './stack';
8
+ export * as Tabs from './tabs';
7
9
  export * as Tooltip from './tooltip';
8
10
  export * from './visually-hidden';
@@ -1,6 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { Stack } from '../index';
3
- import { Box } from '../../box';
4
3
 
5
4
  const meta: Meta< typeof Stack > = {
6
5
  title: 'Design System/Components/Stack',
@@ -9,9 +8,9 @@ const meta: Meta< typeof Stack > = {
9
8
  export default meta;
10
9
 
11
10
  const DemoBox = ( { variant }: { variant?: 'lg' } ) => (
12
- <Box
13
- backgroundColor="brand"
11
+ <div
14
12
  style={ {
13
+ backgroundColor: 'var(--wpds-color-bg-surface-brand)',
15
14
  width: variant === 'lg' ? '150px' : '100px',
16
15
  height: variant === 'lg' ? '150px' : '100px',
17
16
  } }
@@ -22,7 +21,7 @@ type Story = StoryObj< typeof Stack >;
22
21
 
23
22
  export const Default: Story = {
24
23
  args: {
25
- gap: 'sm',
24
+ gap: 'md',
26
25
  children: (
27
26
  <>
28
27
  <DemoBox />
@@ -91,7 +90,7 @@ export const Nested: Story = {
91
90
  children: (
92
91
  <>
93
92
  <DemoBox variant="lg" />
94
- <Stack gap="md">
93
+ <Stack gap="lg">
95
94
  <DemoBox />
96
95
  <DemoBox />
97
96
  </Stack>
@@ -0,0 +1,6 @@
1
+ import { List } from './list';
2
+ import { Panel } from './panel';
3
+ import { Root } from './root';
4
+ import { Tab } from './tab';
5
+
6
+ export { Root, List, Panel, Tab };