@wordpress/ui 0.8.0 → 0.9.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 (357) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +106 -0
  3. package/build/badge/badge.cjs +3 -3
  4. package/build/badge/badge.cjs.map +2 -2
  5. package/build/button/button.cjs +7 -7
  6. package/build/button/button.cjs.map +2 -2
  7. package/build/card/content.cjs +54 -0
  8. package/build/card/content.cjs.map +7 -0
  9. package/build/card/full-bleed.cjs +57 -0
  10. package/build/card/full-bleed.cjs.map +7 -0
  11. package/build/card/header.cjs +54 -0
  12. package/build/card/header.cjs.map +7 -0
  13. package/build/card/index.cjs +43 -0
  14. package/build/card/index.cjs.map +7 -0
  15. package/build/card/root.cjs +73 -0
  16. package/build/card/root.cjs.map +7 -0
  17. package/build/card/title.cjs +55 -0
  18. package/build/card/title.cjs.map +7 -0
  19. package/build/card/types.cjs +19 -0
  20. package/build/card/types.cjs.map +7 -0
  21. package/build/collapsible/index.cjs +37 -0
  22. package/build/collapsible/index.cjs.map +7 -0
  23. package/build/collapsible/panel.cjs +38 -0
  24. package/build/collapsible/panel.cjs.map +7 -0
  25. package/build/collapsible/root.cjs +38 -0
  26. package/build/collapsible/root.cjs.map +7 -0
  27. package/build/collapsible/trigger.cjs +38 -0
  28. package/build/collapsible/trigger.cjs.map +7 -0
  29. package/build/collapsible/types.cjs +19 -0
  30. package/build/collapsible/types.cjs.map +7 -0
  31. package/build/collapsible-card/content.cjs +77 -0
  32. package/build/collapsible-card/content.cjs.map +7 -0
  33. package/build/collapsible-card/header.cjs +102 -0
  34. package/build/collapsible-card/header.cjs.map +7 -0
  35. package/build/collapsible-card/index.cjs +37 -0
  36. package/build/collapsible-card/index.cjs.map +7 -0
  37. package/build/collapsible-card/root.cjs +56 -0
  38. package/build/collapsible-card/root.cjs.map +7 -0
  39. package/build/collapsible-card/types.cjs +19 -0
  40. package/build/collapsible-card/types.cjs.map +7 -0
  41. package/build/dialog/footer.cjs +3 -3
  42. package/build/dialog/footer.cjs.map +2 -2
  43. package/build/dialog/header.cjs +3 -3
  44. package/build/dialog/header.cjs.map +2 -2
  45. package/build/dialog/popup.cjs +3 -3
  46. package/build/dialog/popup.cjs.map +2 -2
  47. package/build/dialog/title.cjs +3 -3
  48. package/build/dialog/title.cjs.map +2 -2
  49. package/build/dialog/types.cjs.map +1 -1
  50. package/build/form/primitives/field/label.cjs +6 -1
  51. package/build/form/primitives/field/label.cjs.map +2 -2
  52. package/build/form/primitives/field/types.cjs.map +1 -1
  53. package/build/form/primitives/fieldset/legend.cjs +5 -1
  54. package/build/form/primitives/fieldset/legend.cjs.map +2 -2
  55. package/build/form/primitives/fieldset/types.cjs.map +1 -1
  56. package/build/form/primitives/input/input.cjs +4 -4
  57. package/build/form/primitives/input/input.cjs.map +2 -2
  58. package/build/form/primitives/input-layout/slot.cjs +3 -2
  59. package/build/form/primitives/input-layout/slot.cjs.map +2 -2
  60. package/build/form/primitives/select/item.cjs +3 -3
  61. package/build/form/primitives/select/item.cjs.map +2 -2
  62. package/build/form/primitives/select/popup.cjs +3 -3
  63. package/build/form/primitives/select/popup.cjs.map +2 -2
  64. package/build/form/primitives/select/trigger.cjs +7 -7
  65. package/build/form/primitives/select/trigger.cjs.map +2 -2
  66. package/build/index.cjs +13 -0
  67. package/build/index.cjs.map +2 -2
  68. package/build/link/index.cjs +31 -0
  69. package/build/link/index.cjs.map +7 -0
  70. package/build/link/link.cjs +125 -0
  71. package/build/link/link.cjs.map +7 -0
  72. package/build/link/types.cjs +19 -0
  73. package/build/link/types.cjs.map +7 -0
  74. package/build/notice/action-button.cjs +3 -3
  75. package/build/notice/action-button.cjs.map +2 -2
  76. package/build/notice/action-link.cjs +17 -18
  77. package/build/notice/action-link.cjs.map +2 -2
  78. package/build/notice/actions.cjs +3 -3
  79. package/build/notice/actions.cjs.map +2 -2
  80. package/build/notice/close-icon.cjs +3 -3
  81. package/build/notice/close-icon.cjs.map +2 -2
  82. package/build/notice/description.cjs +26 -15
  83. package/build/notice/description.cjs.map +3 -3
  84. package/build/notice/root.cjs +3 -3
  85. package/build/notice/root.cjs.map +2 -2
  86. package/build/notice/title.cjs +26 -12
  87. package/build/notice/title.cjs.map +3 -3
  88. package/build/notice/types.cjs.map +1 -1
  89. package/build/tabs/list.cjs +3 -3
  90. package/build/tabs/list.cjs.map +2 -2
  91. package/build/tabs/panel.cjs +3 -3
  92. package/build/tabs/panel.cjs.map +2 -2
  93. package/build/tabs/tab.cjs +3 -3
  94. package/build/tabs/tab.cjs.map +2 -2
  95. package/build/text/index.cjs +31 -0
  96. package/build/text/index.cjs.map +7 -0
  97. package/build/text/text.cjs +65 -0
  98. package/build/text/text.cjs.map +7 -0
  99. package/build/text/types.cjs +19 -0
  100. package/build/text/types.cjs.map +7 -0
  101. package/build/tooltip/popup.cjs +1 -1
  102. package/build/tooltip/popup.cjs.map +1 -1
  103. package/build/visually-hidden/visually-hidden.cjs +3 -3
  104. package/build/visually-hidden/visually-hidden.cjs.map +2 -2
  105. package/build-module/badge/badge.mjs +3 -3
  106. package/build-module/badge/badge.mjs.map +2 -2
  107. package/build-module/button/button.mjs +7 -7
  108. package/build-module/button/button.mjs.map +2 -2
  109. package/build-module/card/content.mjs +29 -0
  110. package/build-module/card/content.mjs.map +7 -0
  111. package/build-module/card/full-bleed.mjs +32 -0
  112. package/build-module/card/full-bleed.mjs.map +7 -0
  113. package/build-module/card/header.mjs +29 -0
  114. package/build-module/card/header.mjs.map +7 -0
  115. package/build-module/card/index.mjs +14 -0
  116. package/build-module/card/index.mjs.map +7 -0
  117. package/build-module/card/root.mjs +38 -0
  118. package/build-module/card/root.mjs.map +7 -0
  119. package/build-module/card/title.mjs +30 -0
  120. package/build-module/card/title.mjs.map +7 -0
  121. package/build-module/card/types.mjs +1 -0
  122. package/build-module/card/types.mjs.map +7 -0
  123. package/build-module/collapsible/index.mjs +10 -0
  124. package/build-module/collapsible/index.mjs.map +7 -0
  125. package/build-module/collapsible/panel.mjs +13 -0
  126. package/build-module/collapsible/panel.mjs.map +7 -0
  127. package/build-module/collapsible/root.mjs +13 -0
  128. package/build-module/collapsible/root.mjs.map +7 -0
  129. package/build-module/collapsible/trigger.mjs +13 -0
  130. package/build-module/collapsible/trigger.mjs.map +7 -0
  131. package/build-module/collapsible/types.mjs +1 -0
  132. package/build-module/collapsible/types.mjs.map +7 -0
  133. package/build-module/collapsible-card/content.mjs +42 -0
  134. package/build-module/collapsible-card/content.mjs.map +7 -0
  135. package/build-module/collapsible-card/header.mjs +67 -0
  136. package/build-module/collapsible-card/header.mjs.map +7 -0
  137. package/build-module/collapsible-card/index.mjs +10 -0
  138. package/build-module/collapsible-card/index.mjs.map +7 -0
  139. package/build-module/collapsible-card/root.mjs +21 -0
  140. package/build-module/collapsible-card/root.mjs.map +7 -0
  141. package/build-module/collapsible-card/types.mjs +1 -0
  142. package/build-module/collapsible-card/types.mjs.map +7 -0
  143. package/build-module/dialog/footer.mjs +3 -3
  144. package/build-module/dialog/footer.mjs.map +2 -2
  145. package/build-module/dialog/header.mjs +3 -3
  146. package/build-module/dialog/header.mjs.map +2 -2
  147. package/build-module/dialog/popup.mjs +3 -3
  148. package/build-module/dialog/popup.mjs.map +2 -2
  149. package/build-module/dialog/title.mjs +3 -3
  150. package/build-module/dialog/title.mjs.map +2 -2
  151. package/build-module/form/primitives/field/label.mjs +6 -1
  152. package/build-module/form/primitives/field/label.mjs.map +2 -2
  153. package/build-module/form/primitives/fieldset/legend.mjs +5 -1
  154. package/build-module/form/primitives/fieldset/legend.mjs.map +2 -2
  155. package/build-module/form/primitives/input/input.mjs +4 -4
  156. package/build-module/form/primitives/input/input.mjs.map +2 -2
  157. package/build-module/form/primitives/input-layout/slot.mjs +3 -2
  158. package/build-module/form/primitives/input-layout/slot.mjs.map +2 -2
  159. package/build-module/form/primitives/select/item.mjs +3 -3
  160. package/build-module/form/primitives/select/item.mjs.map +2 -2
  161. package/build-module/form/primitives/select/popup.mjs +3 -3
  162. package/build-module/form/primitives/select/popup.mjs.map +2 -2
  163. package/build-module/form/primitives/select/trigger.mjs +7 -7
  164. package/build-module/form/primitives/select/trigger.mjs.map +2 -2
  165. package/build-module/index.mjs +8 -0
  166. package/build-module/index.mjs.map +2 -2
  167. package/build-module/link/index.mjs +6 -0
  168. package/build-module/link/index.mjs.map +7 -0
  169. package/build-module/link/link.mjs +90 -0
  170. package/build-module/link/link.mjs.map +7 -0
  171. package/build-module/link/types.mjs +1 -0
  172. package/build-module/link/types.mjs.map +7 -0
  173. package/build-module/notice/action-button.mjs +3 -3
  174. package/build-module/notice/action-button.mjs.map +2 -2
  175. package/build-module/notice/action-link.mjs +17 -18
  176. package/build-module/notice/action-link.mjs.map +2 -2
  177. package/build-module/notice/actions.mjs +3 -3
  178. package/build-module/notice/actions.mjs.map +2 -2
  179. package/build-module/notice/close-icon.mjs +3 -3
  180. package/build-module/notice/close-icon.mjs.map +2 -2
  181. package/build-module/notice/description.mjs +16 -15
  182. package/build-module/notice/description.mjs.map +2 -2
  183. package/build-module/notice/root.mjs +3 -3
  184. package/build-module/notice/root.mjs.map +2 -2
  185. package/build-module/notice/title.mjs +16 -12
  186. package/build-module/notice/title.mjs.map +2 -2
  187. package/build-module/tabs/list.mjs +3 -3
  188. package/build-module/tabs/list.mjs.map +2 -2
  189. package/build-module/tabs/panel.mjs +3 -3
  190. package/build-module/tabs/panel.mjs.map +2 -2
  191. package/build-module/tabs/tab.mjs +3 -3
  192. package/build-module/tabs/tab.mjs.map +2 -2
  193. package/build-module/text/index.mjs +6 -0
  194. package/build-module/text/index.mjs.map +7 -0
  195. package/build-module/text/text.mjs +30 -0
  196. package/build-module/text/text.mjs.map +7 -0
  197. package/build-module/text/types.mjs +1 -0
  198. package/build-module/text/types.mjs.map +7 -0
  199. package/build-module/tooltip/popup.mjs +1 -1
  200. package/build-module/tooltip/popup.mjs.map +1 -1
  201. package/build-module/visually-hidden/visually-hidden.mjs +3 -3
  202. package/build-module/visually-hidden/visually-hidden.mjs.map +2 -2
  203. package/build-types/card/content.d.ts +6 -0
  204. package/build-types/card/content.d.ts.map +1 -0
  205. package/build-types/card/full-bleed.d.ts +9 -0
  206. package/build-types/card/full-bleed.d.ts.map +1 -0
  207. package/build-types/card/header.d.ts +7 -0
  208. package/build-types/card/header.d.ts.map +1 -0
  209. package/build-types/card/index.d.ts +7 -0
  210. package/build-types/card/index.d.ts.map +1 -0
  211. package/build-types/card/root.d.ts +23 -0
  212. package/build-types/card/root.d.ts.map +1 -0
  213. package/build-types/card/stories/index.story.d.ts +22 -0
  214. package/build-types/card/stories/index.story.d.ts.map +1 -0
  215. package/build-types/card/test/index.test.d.ts +2 -0
  216. package/build-types/card/test/index.test.d.ts.map +1 -0
  217. package/build-types/card/title.d.ts +7 -0
  218. package/build-types/card/title.d.ts.map +1 -0
  219. package/build-types/card/types.d.ts +34 -0
  220. package/build-types/card/types.d.ts.map +1 -0
  221. package/build-types/collapsible/index.d.ts +5 -0
  222. package/build-types/collapsible/index.d.ts.map +1 -0
  223. package/build-types/collapsible/panel.d.ts +16 -0
  224. package/build-types/collapsible/panel.d.ts.map +1 -0
  225. package/build-types/collapsible/root.d.ts +15 -0
  226. package/build-types/collapsible/root.d.ts.map +1 -0
  227. package/build-types/collapsible/stories/index.story.d.ts +18 -0
  228. package/build-types/collapsible/stories/index.story.d.ts.map +1 -0
  229. package/build-types/collapsible/test/index.test.d.ts +2 -0
  230. package/build-types/collapsible/test/index.test.d.ts.map +1 -0
  231. package/build-types/collapsible/trigger.d.ts +15 -0
  232. package/build-types/collapsible/trigger.d.ts.map +1 -0
  233. package/build-types/collapsible/types.d.ts +22 -0
  234. package/build-types/collapsible/types.d.ts.map +1 -0
  235. package/build-types/collapsible-card/content.d.ts +7 -0
  236. package/build-types/collapsible-card/content.d.ts.map +1 -0
  237. package/build-types/collapsible-card/header.d.ts +12 -0
  238. package/build-types/collapsible-card/header.d.ts.map +1 -0
  239. package/build-types/collapsible-card/index.d.ts +5 -0
  240. package/build-types/collapsible-card/index.d.ts.map +1 -0
  241. package/build-types/collapsible-card/root.d.ts +24 -0
  242. package/build-types/collapsible-card/root.d.ts.map +1 -0
  243. package/build-types/collapsible-card/stories/index.story.d.ts +28 -0
  244. package/build-types/collapsible-card/stories/index.story.d.ts.map +1 -0
  245. package/build-types/collapsible-card/test/index.test.d.ts +2 -0
  246. package/build-types/collapsible-card/test/index.test.d.ts.map +1 -0
  247. package/build-types/collapsible-card/types.d.ts +52 -0
  248. package/build-types/collapsible-card/types.d.ts.map +1 -0
  249. package/build-types/dialog/types.d.ts +3 -3
  250. package/build-types/form/primitives/field/label.d.ts +1 -0
  251. package/build-types/form/primitives/field/label.d.ts.map +1 -1
  252. package/build-types/form/primitives/field/stories/index.story.d.ts +5 -0
  253. package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
  254. package/build-types/form/primitives/field/types.d.ts +7 -0
  255. package/build-types/form/primitives/field/types.d.ts.map +1 -1
  256. package/build-types/form/primitives/fieldset/legend.d.ts +1 -0
  257. package/build-types/form/primitives/fieldset/legend.d.ts.map +1 -1
  258. package/build-types/form/primitives/fieldset/stories/index.story.d.ts +5 -0
  259. package/build-types/form/primitives/fieldset/stories/index.story.d.ts.map +1 -1
  260. package/build-types/form/primitives/fieldset/types.d.ts +7 -0
  261. package/build-types/form/primitives/fieldset/types.d.ts.map +1 -1
  262. package/build-types/form/primitives/input-layout/slot.d.ts.map +1 -1
  263. package/build-types/index.d.ts +5 -0
  264. package/build-types/index.d.ts.map +1 -1
  265. package/build-types/link/index.d.ts +2 -0
  266. package/build-types/link/index.d.ts.map +1 -0
  267. package/build-types/link/link.d.ts +7 -0
  268. package/build-types/link/link.d.ts.map +1 -0
  269. package/build-types/link/stories/index.story.d.ts +18 -0
  270. package/build-types/link/stories/index.story.d.ts.map +1 -0
  271. package/build-types/link/test/index.test.d.ts +2 -0
  272. package/build-types/link/test/index.test.d.ts.map +1 -0
  273. package/build-types/link/types.d.ts +33 -0
  274. package/build-types/link/types.d.ts.map +1 -0
  275. package/build-types/notice/action-link.d.ts +0 -2
  276. package/build-types/notice/action-link.d.ts.map +1 -1
  277. package/build-types/notice/description.d.ts +1 -1
  278. package/build-types/notice/description.d.ts.map +1 -1
  279. package/build-types/notice/title.d.ts.map +1 -1
  280. package/build-types/notice/types.d.ts +3 -2
  281. package/build-types/notice/types.d.ts.map +1 -1
  282. package/build-types/text/index.d.ts +2 -0
  283. package/build-types/text/index.d.ts.map +1 -0
  284. package/build-types/text/stories/index.story.d.ts +9 -0
  285. package/build-types/text/stories/index.story.d.ts.map +1 -0
  286. package/build-types/text/test/index.test.d.ts +2 -0
  287. package/build-types/text/test/index.test.d.ts.map +1 -0
  288. package/build-types/text/text.d.ts +7 -0
  289. package/build-types/text/text.d.ts.map +1 -0
  290. package/build-types/text/types.d.ts +15 -0
  291. package/build-types/text/types.d.ts.map +1 -0
  292. package/package.json +14 -14
  293. package/src/badge/style.module.css +5 -2
  294. package/src/button/style.module.css +2 -0
  295. package/src/card/content.tsx +20 -0
  296. package/src/card/full-bleed.tsx +26 -0
  297. package/src/card/header.tsx +21 -0
  298. package/src/card/index.ts +7 -0
  299. package/src/card/root.tsx +42 -0
  300. package/src/card/stories/index.story.tsx +128 -0
  301. package/src/card/style.module.css +48 -0
  302. package/src/card/test/index.test.tsx +96 -0
  303. package/src/card/title.tsx +22 -0
  304. package/src/card/types.ts +38 -0
  305. package/src/collapsible/index.ts +5 -0
  306. package/src/collapsible/panel.tsx +16 -0
  307. package/src/collapsible/root.tsx +15 -0
  308. package/src/collapsible/stories/index.story.tsx +108 -0
  309. package/src/collapsible/test/index.test.tsx +228 -0
  310. package/src/collapsible/trigger.tsx +15 -0
  311. package/src/collapsible/types.ts +24 -0
  312. package/src/collapsible-card/content.tsx +33 -0
  313. package/src/collapsible-card/header.tsx +53 -0
  314. package/src/collapsible-card/index.ts +5 -0
  315. package/src/collapsible-card/root.tsx +37 -0
  316. package/src/collapsible-card/stories/index.story.tsx +207 -0
  317. package/src/collapsible-card/style.module.css +78 -0
  318. package/src/collapsible-card/test/index.test.tsx +181 -0
  319. package/src/collapsible-card/types.ts +54 -0
  320. package/src/dialog/style.module.css +5 -5
  321. package/src/dialog/types.ts +3 -3
  322. package/src/form/primitives/field/label.tsx +9 -1
  323. package/src/form/primitives/field/stories/index.story.tsx +17 -0
  324. package/src/form/primitives/field/test/index.test.tsx +13 -0
  325. package/src/form/primitives/field/types.ts +7 -0
  326. package/src/form/primitives/fieldset/legend.tsx +8 -1
  327. package/src/form/primitives/fieldset/stories/index.story.tsx +20 -0
  328. package/src/form/primitives/fieldset/test/index.test.tsx +14 -0
  329. package/src/form/primitives/fieldset/types.ts +7 -0
  330. package/src/form/primitives/input-layout/slot.tsx +6 -2
  331. package/src/index.ts +5 -0
  332. package/src/link/index.ts +1 -0
  333. package/src/link/link.tsx +73 -0
  334. package/src/link/stories/index.story.tsx +92 -0
  335. package/src/link/style.module.css +68 -0
  336. package/src/link/test/index.test.tsx +93 -0
  337. package/src/link/types.ts +36 -0
  338. package/src/notice/action-link.tsx +12 -18
  339. package/src/notice/description.tsx +12 -14
  340. package/src/notice/style.module.css +9 -22
  341. package/src/notice/test/index.test.tsx +2 -2
  342. package/src/notice/title.tsx +11 -10
  343. package/src/notice/types.ts +3 -2
  344. package/src/tabs/style.module.css +1 -1
  345. package/src/text/index.ts +1 -0
  346. package/src/text/stories/index.story.tsx +68 -0
  347. package/src/text/style.module.css +67 -0
  348. package/src/text/test/index.test.tsx +46 -0
  349. package/src/text/text.tsx +25 -0
  350. package/src/text/types.ts +25 -0
  351. package/src/tooltip/popup.tsx +1 -1
  352. package/src/utils/css/focus.module.css +4 -2
  353. package/src/utils/css/item-popup.module.css +1 -0
  354. package/src/utils/css/select-trigger.module.css +1 -0
  355. package/src/visually-hidden/style.module.css +1 -0
  356. package/AGENTS.md +0 -9
  357. package/CLAUDE.md +0 -1
@@ -0,0 +1,228 @@
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 * as Collapsible from '../index';
5
+
6
+ function UncontrolledCollapsible( {
7
+ defaultOpen,
8
+ disabled,
9
+ }: {
10
+ defaultOpen?: boolean;
11
+ disabled?: boolean;
12
+ } ) {
13
+ return (
14
+ <Collapsible.Root defaultOpen={ defaultOpen } disabled={ disabled }>
15
+ <Collapsible.Trigger>Toggle</Collapsible.Trigger>
16
+ <Collapsible.Panel>Panel content</Collapsible.Panel>
17
+ </Collapsible.Root>
18
+ );
19
+ }
20
+
21
+ function ControlledCollapsible( {
22
+ onOpenChange,
23
+ }: {
24
+ onOpenChange?: ( open: boolean ) => void;
25
+ } ) {
26
+ const [ open, setOpen ] = useState( false );
27
+ return (
28
+ <Collapsible.Root
29
+ open={ open }
30
+ onOpenChange={ ( nextOpen ) => {
31
+ setOpen( nextOpen );
32
+ onOpenChange?.( nextOpen );
33
+ } }
34
+ >
35
+ <Collapsible.Trigger>Toggle</Collapsible.Trigger>
36
+ <Collapsible.Panel>Controlled panel</Collapsible.Panel>
37
+ </Collapsible.Root>
38
+ );
39
+ }
40
+
41
+ describe( 'Collapsible', () => {
42
+ describe( 'ref forwarding', () => {
43
+ it( 'forwards ref on Root', () => {
44
+ const ref = createRef< HTMLDivElement >();
45
+ render(
46
+ <Collapsible.Root ref={ ref }>
47
+ <Collapsible.Trigger>Toggle</Collapsible.Trigger>
48
+ <Collapsible.Panel>Content</Collapsible.Panel>
49
+ </Collapsible.Root>
50
+ );
51
+ expect( ref.current ).toBeInstanceOf( HTMLDivElement );
52
+ } );
53
+
54
+ it( 'forwards ref on Trigger', () => {
55
+ const ref = createRef< HTMLButtonElement >();
56
+ render(
57
+ <Collapsible.Root>
58
+ <Collapsible.Trigger ref={ ref }>
59
+ Toggle
60
+ </Collapsible.Trigger>
61
+ <Collapsible.Panel>Content</Collapsible.Panel>
62
+ </Collapsible.Root>
63
+ );
64
+ expect( ref.current ).toBeInstanceOf( HTMLButtonElement );
65
+ } );
66
+
67
+ it( 'forwards ref on Panel', () => {
68
+ const ref = createRef< HTMLDivElement >();
69
+ render(
70
+ <Collapsible.Root defaultOpen>
71
+ <Collapsible.Trigger>Toggle</Collapsible.Trigger>
72
+ <Collapsible.Panel ref={ ref }>Content</Collapsible.Panel>
73
+ </Collapsible.Root>
74
+ );
75
+ expect( ref.current ).toBeInstanceOf( HTMLDivElement );
76
+ } );
77
+ } );
78
+
79
+ describe( 'uncontrolled', () => {
80
+ it( 'is collapsed by default', () => {
81
+ render( <UncontrolledCollapsible /> );
82
+ expect(
83
+ screen.queryByText( 'Panel content' )
84
+ ).not.toBeInTheDocument();
85
+ } );
86
+
87
+ it( 'shows panel when defaultOpen is true', () => {
88
+ render( <UncontrolledCollapsible defaultOpen /> );
89
+ expect( screen.getByText( 'Panel content' ) ).toBeVisible();
90
+ } );
91
+
92
+ it( 'toggles panel on trigger click', async () => {
93
+ const user = userEvent.setup();
94
+ render( <UncontrolledCollapsible /> );
95
+
96
+ expect(
97
+ screen.queryByText( 'Panel content' )
98
+ ).not.toBeInTheDocument();
99
+
100
+ await user.click(
101
+ screen.getByRole( 'button', { name: 'Toggle' } )
102
+ );
103
+ expect( screen.getByText( 'Panel content' ) ).toBeVisible();
104
+
105
+ await user.click(
106
+ screen.getByRole( 'button', { name: 'Toggle' } )
107
+ );
108
+ expect(
109
+ screen.queryByText( 'Panel content' )
110
+ ).not.toBeInTheDocument();
111
+ } );
112
+ } );
113
+
114
+ describe( 'controlled', () => {
115
+ it( 'calls onOpenChange when toggled', async () => {
116
+ const onOpenChange = jest.fn();
117
+ const user = userEvent.setup();
118
+
119
+ render( <ControlledCollapsible onOpenChange={ onOpenChange } /> );
120
+
121
+ await user.click(
122
+ screen.getByRole( 'button', { name: 'Toggle' } )
123
+ );
124
+ expect( onOpenChange ).toHaveBeenCalledWith( true );
125
+
126
+ await user.click(
127
+ screen.getByRole( 'button', { name: 'Toggle' } )
128
+ );
129
+ expect( onOpenChange ).toHaveBeenCalledWith( false );
130
+ } );
131
+ } );
132
+
133
+ describe( 'disabled', () => {
134
+ it( 'does not toggle when disabled', async () => {
135
+ const user = userEvent.setup();
136
+ render( <UncontrolledCollapsible defaultOpen disabled /> );
137
+
138
+ expect( screen.getByText( 'Panel content' ) ).toBeVisible();
139
+
140
+ await user.click(
141
+ screen.getByRole( 'button', { name: 'Toggle' } )
142
+ );
143
+ expect( screen.getByText( 'Panel content' ) ).toBeVisible();
144
+ } );
145
+ } );
146
+
147
+ describe( 'render prop', () => {
148
+ it( 'supports render prop on Root', () => {
149
+ const ref = createRef< HTMLDivElement >();
150
+ render(
151
+ <Collapsible.Root ref={ ref } render={ <section /> }>
152
+ <Collapsible.Trigger>Toggle</Collapsible.Trigger>
153
+ <Collapsible.Panel>Content</Collapsible.Panel>
154
+ </Collapsible.Root>
155
+ );
156
+ expect( ref.current?.tagName ).toBe( 'SECTION' );
157
+ } );
158
+
159
+ it( 'supports render prop on Trigger', () => {
160
+ render(
161
+ <Collapsible.Root>
162
+ <Collapsible.Trigger
163
+ nativeButton={ false }
164
+ render={ <div /> }
165
+ >
166
+ Toggle
167
+ </Collapsible.Trigger>
168
+ <Collapsible.Panel>Content</Collapsible.Panel>
169
+ </Collapsible.Root>
170
+ );
171
+ const trigger = screen.getByRole( 'button', { name: 'Toggle' } );
172
+ expect( trigger.tagName ).toBe( 'DIV' );
173
+ } );
174
+
175
+ it( 'supports render prop on Panel', () => {
176
+ const ref = createRef< HTMLDivElement >();
177
+ render(
178
+ <Collapsible.Root defaultOpen>
179
+ <Collapsible.Trigger>Toggle</Collapsible.Trigger>
180
+ <Collapsible.Panel ref={ ref } render={ <section /> }>
181
+ Content
182
+ </Collapsible.Panel>
183
+ </Collapsible.Root>
184
+ );
185
+ expect( ref.current?.tagName ).toBe( 'SECTION' );
186
+ } );
187
+ } );
188
+
189
+ describe( 'custom className', () => {
190
+ it( 'applies className to Root', () => {
191
+ const ref = createRef< HTMLDivElement >();
192
+ render(
193
+ <Collapsible.Root ref={ ref } className="custom-root">
194
+ <Collapsible.Trigger>Toggle</Collapsible.Trigger>
195
+ <Collapsible.Panel>Content</Collapsible.Panel>
196
+ </Collapsible.Root>
197
+ );
198
+ expect( ref.current ).toHaveClass( 'custom-root' );
199
+ } );
200
+
201
+ it( 'applies className to Trigger', () => {
202
+ render(
203
+ <Collapsible.Root>
204
+ <Collapsible.Trigger className="custom-trigger">
205
+ Toggle
206
+ </Collapsible.Trigger>
207
+ <Collapsible.Panel>Content</Collapsible.Panel>
208
+ </Collapsible.Root>
209
+ );
210
+ expect(
211
+ screen.getByRole( 'button', { name: 'Toggle' } )
212
+ ).toHaveClass( 'custom-trigger' );
213
+ } );
214
+
215
+ it( 'applies className to Panel', () => {
216
+ const ref = createRef< HTMLDivElement >();
217
+ render(
218
+ <Collapsible.Root defaultOpen>
219
+ <Collapsible.Trigger>Toggle</Collapsible.Trigger>
220
+ <Collapsible.Panel ref={ ref } className="custom-panel">
221
+ Content
222
+ </Collapsible.Panel>
223
+ </Collapsible.Root>
224
+ );
225
+ expect( ref.current ).toHaveClass( 'custom-panel' );
226
+ } );
227
+ } );
228
+ } );
@@ -0,0 +1,15 @@
1
+ import { Collapsible as _Collapsible } from '@base-ui/react/collapsible';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import type { TriggerProps } from './types';
4
+
5
+ /**
6
+ * A button that opens and closes the collapsible panel.
7
+ *
8
+ * `Collapsible` is a collection of React components that combine to render
9
+ * a collapsible panel controlled by a button.
10
+ */
11
+ export const Trigger = forwardRef< HTMLButtonElement, TriggerProps >(
12
+ function CollapsibleTrigger( props, forwardedRef ) {
13
+ return <_Collapsible.Trigger ref={ forwardedRef } { ...props } />;
14
+ }
15
+ );
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Collapsible as _Collapsible } from '@base-ui/react/collapsible';
3
+ import type { ComponentProps } from '../utils/types';
4
+
5
+ export type RootProps = ComponentProps< typeof _Collapsible.Root > & {
6
+ /**
7
+ * The content to be rendered inside the component.
8
+ */
9
+ children?: ReactNode;
10
+ };
11
+
12
+ export type TriggerProps = ComponentProps< typeof _Collapsible.Trigger > & {
13
+ /**
14
+ * The content to be rendered inside the component.
15
+ */
16
+ children?: ReactNode;
17
+ };
18
+
19
+ export type PanelProps = ComponentProps< typeof _Collapsible.Panel > & {
20
+ /**
21
+ * The content to be rendered inside the component.
22
+ */
23
+ children?: ReactNode;
24
+ };
@@ -0,0 +1,33 @@
1
+ import { forwardRef } from '@wordpress/element';
2
+ import clsx from 'clsx';
3
+ import * as Card from '../card';
4
+ import * as Collapsible from '../collapsible';
5
+ import styles from './style.module.css';
6
+ import type { ContentProps } from './types';
7
+
8
+ /**
9
+ * The collapsible content area of the card. Hidden when collapsed,
10
+ * visible when expanded.
11
+ */
12
+ export const Content = forwardRef< HTMLDivElement, ContentProps >(
13
+ function CollapsibleCardContent(
14
+ { className, render, children, hiddenUntilFound = true, ...restProps },
15
+ ref
16
+ ) {
17
+ return (
18
+ <Collapsible.Panel
19
+ ref={ ref }
20
+ className={ clsx( styles.content, className ) }
21
+ hiddenUntilFound={ hiddenUntilFound }
22
+ { ...restProps }
23
+ >
24
+ <Card.Content
25
+ className={ styles[ 'content-inner' ] }
26
+ render={ render }
27
+ >
28
+ { children }
29
+ </Card.Content>
30
+ </Collapsible.Panel>
31
+ );
32
+ }
33
+ );
@@ -0,0 +1,53 @@
1
+ import clsx from 'clsx';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import { chevronDown } from '@wordpress/icons';
4
+ import * as Card from '../card';
5
+ import * as Collapsible from '../collapsible';
6
+ import { Icon } from '../icon';
7
+ import styles from './style.module.css';
8
+ import focusStyles from '../utils/css/focus.module.css';
9
+ import type { HeaderProps } from './types';
10
+
11
+ /**
12
+ * The header of a collapsible card. Always visible, and acts as the
13
+ * toggle trigger — clicking anywhere on it expands or collapses the
14
+ * card's content.
15
+ *
16
+ * Avoid placing interactive elements (buttons, links, inputs) inside the
17
+ * header, since the entire area is clickable and their events will bubble
18
+ * to trigger the collapse toggle.
19
+ */
20
+ export const Header = forwardRef< HTMLDivElement, HeaderProps >(
21
+ function CollapsibleCardHeader(
22
+ { children, className, render, ...restProps },
23
+ ref
24
+ ) {
25
+ return (
26
+ <Collapsible.Trigger
27
+ className={ clsx( styles.header, className ) }
28
+ render={
29
+ <Card.Header
30
+ ref={ ref }
31
+ render={ render }
32
+ { ...restProps }
33
+ />
34
+ }
35
+ nativeButton={ false }
36
+ >
37
+ <div className={ styles[ 'header-content' ] }>{ children }</div>
38
+ <div className={ styles[ 'header-trigger-wrapper' ] }>
39
+ <Icon
40
+ icon={ chevronDown }
41
+ className={ clsx(
42
+ styles[ 'header-trigger' ],
43
+ // While the interactive trigger element is the whole header,
44
+ // the focus ring will be displayed only on the icon to visually
45
+ // emulate it being the button.
46
+ focusStyles[ 'outset-ring--focus-parent-visible' ]
47
+ ) }
48
+ />
49
+ </div>
50
+ </Collapsible.Trigger>
51
+ );
52
+ }
53
+ );
@@ -0,0 +1,5 @@
1
+ import { Root } from './root';
2
+ import { Header } from './header';
3
+ import { Content } from './content';
4
+
5
+ export { Root, Header, Content };
@@ -0,0 +1,37 @@
1
+ import { forwardRef } from '@wordpress/element';
2
+ import * as Card from '../card';
3
+ import * as Collapsible from '../collapsible';
4
+ import type { RootProps } from './types';
5
+
6
+ /**
7
+ * A card that can be expanded and collapsed. When collapsed, only the
8
+ * header is visible.
9
+ *
10
+ * ```jsx
11
+ * import { CollapsibleCard, Card } from '@wordpress/ui';
12
+ *
13
+ * function MyComponent() {
14
+ * return (
15
+ * <CollapsibleCard.Root defaultOpen>
16
+ * <CollapsibleCard.Header>
17
+ * <Card.Title>Heading</Card.Title>
18
+ * </CollapsibleCard.Header>
19
+ * <CollapsibleCard.Content>
20
+ * <p>Collapsible content here.</p>
21
+ * </CollapsibleCard.Content>
22
+ * </CollapsibleCard.Root>
23
+ * );
24
+ * }
25
+ * ```
26
+ */
27
+ export const Root = forwardRef< HTMLDivElement, RootProps >(
28
+ function CollapsibleCardRoot( { render, ...restProps }, ref ) {
29
+ return (
30
+ <Collapsible.Root
31
+ ref={ ref }
32
+ render={ <Card.Root render={ render } /> }
33
+ { ...restProps }
34
+ />
35
+ );
36
+ }
37
+ );
@@ -0,0 +1,207 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import * as Card from '../../card';
3
+ import * as CollapsibleCard from '../index';
4
+
5
+ /**
6
+ * Temporary text component for story examples. This will be replaced by an
7
+ * official DS `<Text />` component once it's available.
8
+ */
9
+ function Text( { children }: { children: React.ReactNode } ) {
10
+ return (
11
+ <p
12
+ style={ {
13
+ margin: 0,
14
+ fontFamily: 'var(--wpds-font-family-body)',
15
+ fontSize: 'var(--wpds-font-size-md)',
16
+ fontWeight: 'var(--wpds-font-weight-regular)',
17
+ lineHeight: 'var(--wpds-font-line-height-sm)',
18
+ textWrap: 'pretty',
19
+ color: 'var(--wpds-color-fg-content-neutral-weak)',
20
+ } }
21
+ >
22
+ { children }
23
+ </p>
24
+ );
25
+ }
26
+
27
+ const meta: Meta< typeof CollapsibleCard.Root > = {
28
+ title: 'Design System/Components/CollapsibleCard',
29
+ component: CollapsibleCard.Root,
30
+ subcomponents: {
31
+ 'CollapsibleCard.Header': CollapsibleCard.Header,
32
+ 'CollapsibleCard.Content': CollapsibleCard.Content,
33
+ },
34
+ };
35
+ export default meta;
36
+
37
+ type Story = StoryObj< typeof CollapsibleCard.Root >;
38
+
39
+ /**
40
+ * A collapsible card that is open by default.
41
+ */
42
+ export const Default: Story = {
43
+ args: {
44
+ children: (
45
+ <>
46
+ <CollapsibleCard.Header>
47
+ <Card.Title>
48
+ Collapsible card (closed by default)
49
+ </Card.Title>
50
+ </CollapsibleCard.Header>
51
+ <CollapsibleCard.Content>
52
+ <Text>
53
+ This is the collapsible content area. It can contain any
54
+ elements, just like a regular Card.Content.
55
+ </Text>
56
+ <Text>
57
+ When collapsed, only the header and chevron are visible.
58
+ </Text>
59
+ </CollapsibleCard.Content>
60
+ </>
61
+ ),
62
+ },
63
+ };
64
+
65
+ /**
66
+ * A collapsible card that starts collapsed.
67
+ */
68
+ export const InitiallyOpened: Story = {
69
+ // `defaultOpen` (uncontrolled) and `open` (controlled) should not be
70
+ // used together — disable the `open` control to avoid confusion.
71
+ argTypes: { open: { control: false } },
72
+ args: {
73
+ ...Default.args,
74
+ defaultOpen: true,
75
+ children: (
76
+ <>
77
+ <CollapsibleCard.Header>
78
+ <Card.Title>Collapsed by default</Card.Title>
79
+ </CollapsibleCard.Header>
80
+ <CollapsibleCard.Content>
81
+ <Text>This content was hidden until you expanded it.</Text>
82
+ </CollapsibleCard.Content>
83
+ </>
84
+ ),
85
+ },
86
+ };
87
+
88
+ /**
89
+ * A disabled collapsible card cannot be toggled by the user.
90
+ */
91
+ export const Disabled: Story = {
92
+ args: {
93
+ ...Default.args,
94
+ disabled: true,
95
+ children: (
96
+ <>
97
+ <CollapsibleCard.Header>
98
+ <Card.Title>Disabled card</Card.Title>
99
+ </CollapsibleCard.Header>
100
+ <CollapsibleCard.Content>
101
+ <Text>The header is not interactive when disabled.</Text>
102
+ </CollapsibleCard.Content>
103
+ </>
104
+ ),
105
+ },
106
+ };
107
+
108
+ /**
109
+ * Multiple collapsible cards stacked vertically, simulating a typical
110
+ * settings-panel or FAQ-style layout.
111
+ */
112
+ export const Stacked: Story = {
113
+ parameters: { controls: { disable: true } },
114
+ render: () => (
115
+ <div
116
+ style={ {
117
+ display: 'flex',
118
+ flexDirection: 'column',
119
+ gap: 'var(--wpds-dimension-gap-lg)',
120
+ } }
121
+ >
122
+ { [
123
+ 'General',
124
+ 'Advanced',
125
+ 'Accessibility',
126
+ 'Performance',
127
+ 'Privacy',
128
+ 'Notifications',
129
+ ].map( ( title ) => (
130
+ <CollapsibleCard.Root key={ title }>
131
+ <CollapsibleCard.Header>
132
+ <Card.Title>{ title }</Card.Title>
133
+ </CollapsibleCard.Header>
134
+ <CollapsibleCard.Content>
135
+ <Text>
136
+ Configure all { title.toLowerCase() } settings for
137
+ your site. Changes here affect how your site behaves
138
+ across all pages and posts.
139
+ </Text>
140
+ <Text>
141
+ Review each option carefully before saving. Some
142
+ changes may require a page reload to take effect.
143
+ Hover over individual options for more details about
144
+ what they control.
145
+ </Text>
146
+ <Text>
147
+ If you&apos;re unsure about a setting, you can
148
+ always reset to defaults using the button at the
149
+ bottom of this section. Your previous configuration
150
+ will be saved as a backup.
151
+ </Text>
152
+ </CollapsibleCard.Content>
153
+ </CollapsibleCard.Root>
154
+ ) ) }
155
+ </div>
156
+ ),
157
+ };
158
+
159
+ /**
160
+ * Visual comparison: a `CollapsibleCard` (open) next to a regular `Card`
161
+ * to verify identical spacing and layout.
162
+ */
163
+ export const ComparedToCard: Story = {
164
+ // `defaultOpen` (uncontrolled) and `open` (controlled) should not be
165
+ // used together — disable the `open` control to avoid confusion.
166
+ argTypes: { open: { control: false } },
167
+ args: {
168
+ ...Default.args,
169
+ defaultOpen: true,
170
+ },
171
+ render: ( { open, defaultOpen, onOpenChange, disabled, ...restArgs } ) => (
172
+ <div
173
+ style={ {
174
+ display: 'flex',
175
+ flexDirection: 'column',
176
+ gap: 'var( --wpds-dimension-gap-lg )',
177
+ } }
178
+ >
179
+ <CollapsibleCard.Root
180
+ open={ open }
181
+ defaultOpen={ defaultOpen }
182
+ onOpenChange={ onOpenChange }
183
+ disabled={ disabled }
184
+ { ...restArgs }
185
+ >
186
+ <CollapsibleCard.Header>
187
+ <Card.Title>CollapsibleCard (open)</Card.Title>
188
+ </CollapsibleCard.Header>
189
+ <CollapsibleCard.Content>
190
+ <Text>
191
+ Content should align with the regular card below.
192
+ </Text>
193
+ </CollapsibleCard.Content>
194
+ </CollapsibleCard.Root>
195
+ <Card.Root { ...restArgs }>
196
+ <Card.Header>
197
+ <Card.Title>Regular Card</Card.Title>
198
+ </Card.Header>
199
+ <Card.Content>
200
+ <Text>
201
+ Content should align with the collapsible card above.
202
+ </Text>
203
+ </Card.Content>
204
+ </Card.Root>
205
+ </div>
206
+ ),
207
+ };
@@ -0,0 +1,78 @@
1
+ @layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;
2
+
3
+ @layer wp-ui-components {
4
+ .header-content {
5
+ flex: 1;
6
+ min-width: 0;
7
+ }
8
+
9
+ .header-trigger-wrapper {
10
+ flex-shrink: 0;
11
+ align-self: center;
12
+ /* Prevent the trigger from making the header taller than its content,
13
+ preserving the same metrics between `Card` and `CollapsibleCard`. */
14
+ max-height: 0;
15
+ overflow: visible;
16
+ }
17
+
18
+ .header-trigger {
19
+ /* Offset by half the button's own height so it visually centers
20
+ at the wrapper's midpoint (which `align-self: center` places at
21
+ the vertical center of the header). */
22
+ translate: 0 -50%;
23
+
24
+ /* For an outline that looks like `IconButton`'s */
25
+ border-radius: var(--wpds-border-radius-sm);
26
+
27
+ @media not (prefers-reduced-motion) {
28
+ transition: rotate 0.15s ease-out;
29
+ }
30
+ }
31
+
32
+ .header[data-panel-open] .header-trigger {
33
+ rotate: 180deg;
34
+ }
35
+
36
+ /* Simulate disabled button styles */
37
+ .header[data-disabled] .header-trigger {
38
+ color: var(--wpds-color-fg-interactive-neutral-disabled);
39
+ }
40
+
41
+ .content {
42
+ height: var(--collapsible-panel-height);
43
+ overflow: hidden;
44
+ margin-block-start: var(--wp-ui-card-header-content-margin);
45
+
46
+ &[hidden]:not([hidden="until-found"]) {
47
+ display: none;
48
+ }
49
+
50
+ &[data-starting-style],
51
+ &[data-ending-style] {
52
+ height: 0;
53
+ }
54
+
55
+ @media not (prefers-reduced-motion) {
56
+ transition: all 150ms ease-out;
57
+ }
58
+ }
59
+
60
+ }
61
+
62
+ @layer wp-ui-compositions {
63
+ .content-inner {
64
+ padding-block-start: 0;
65
+ }
66
+
67
+ .header {
68
+ display: flex;
69
+ flex-direction: row;
70
+ align-items: stretch;
71
+ gap: var(--wpds-dimension-gap-sm);
72
+ outline: none;
73
+
74
+ &:not([data-disabled]) {
75
+ cursor: var(--wpds-cursor-control);
76
+ }
77
+ }
78
+ }