@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
@@ -17,10 +17,11 @@
17
17
  --wp-ui-button-foreground-color: var(--wpds-color-fg-interactive-brand-strong);
18
18
  --wp-ui-button-foreground-color-active: var(--wpds-color-fg-interactive-brand-strong-active);
19
19
  --wp-ui-button-foreground-color-disabled: var(--wpds-color-fg-interactive-neutral-strong-disabled);
20
- --wp-ui-button-padding-inline: calc(3 * var(--wpds-dimension-base)); /* TODO: Create new interactive padding tokens */
20
+ --wp-ui-button-padding-inline: var(--wpds-dimension-padding-md);
21
21
  --wp-ui-button-height: 40px;
22
22
  --wp-ui-button-aspect-ratio: auto; /* Useful for overrides such as icon buttons */
23
23
  --wp-ui-button-font-size: var(--wpds-font-size-md);
24
+ --wp-ui-button-min-width: calc(4ch + 2 * var(--wp-ui-button-padding-inline));
24
25
 
25
26
  /* by default, borders have the same color as the background */
26
27
  --wp-ui-button-border-color: var(--wp-ui-button-background-color);
@@ -32,8 +33,9 @@
32
33
  display: inline-flex;
33
34
  justify-content: center;
34
35
  align-items: center;
35
- gap: calc(2 * var(--wpds-dimension-base)); /* TODO: Consider new interactive/control gap tokens */
36
+ gap: var(--wpds-dimension-gap-sm);
36
37
  aspect-ratio: var(--wp-ui-button-aspect-ratio);
38
+ min-width: var(--wp-ui-button-min-width);
37
39
  height: var(--wp-ui-button-height);
38
40
  padding-inline: var(--wp-ui-button-padding-inline);
39
41
  border-style: solid;
@@ -68,20 +70,22 @@
68
70
  text-decoration: inherit;
69
71
  }
70
72
 
71
- /* States */
72
- &:not([aria-disabled="true"]):is(:hover, :active, :focus) {
73
+ /* States — use property declarations (not variable reassignment) so that
74
+ higher CSS layers can safely override the custom property values without
75
+ breaking state handling. */
76
+ &:not([data-disabled]):is(:hover, :active, :focus) {
73
77
  /* hover/active/focus states apply when the button is not disabled. A non
74
78
  disabled, loading button will have hover/active/focus styles */
75
- --wp-ui-button-background-color: var(--wp-ui-button-background-color-active);
76
- --wp-ui-button-foreground-color: var(--wp-ui-button-foreground-color-active);
77
- --wp-ui-button-border-color: var(--wp-ui-button-border-color-active);
79
+ background-color: var(--wp-ui-button-background-color-active);
80
+ color: var(--wp-ui-button-foreground-color-active);
81
+ border-color: var(--wp-ui-button-border-color-active);
78
82
  }
79
83
 
80
- &[aria-disabled="true"]:not(.is-loading) {
84
+ &[data-disabled]:not(.is-loading) {
81
85
  /* A loading button, even when disabled, won't "look" disabled */
82
- --wp-ui-button-background-color: var(--wp-ui-button-background-color-disabled);
83
- --wp-ui-button-foreground-color: var(--wp-ui-button-foreground-color-disabled);
84
- --wp-ui-button-border-color: var(--wp-ui-button-border-color-disabled);
86
+ background-color: var(--wp-ui-button-background-color-disabled);
87
+ color: var(--wp-ui-button-foreground-color-disabled);
88
+ border-color: var(--wp-ui-button-border-color-disabled);
85
89
 
86
90
  @media ( forced-colors: active ) {
87
91
  border-color: GrayText;
@@ -119,7 +123,7 @@
119
123
  }
120
124
 
121
125
  .is-small {
122
- --wp-ui-button-padding-inline: calc(2 * var(--wpds-dimension-base)); /* TODO: Create new interactive padding tokens */
126
+ --wp-ui-button-padding-inline: var(--wpds-dimension-padding-sm);
123
127
  --wp-ui-button-height: 24px;
124
128
  }
125
129
 
@@ -188,6 +192,7 @@
188
192
  }
189
193
 
190
194
  .is-unstyled {
195
+ min-width: unset;
191
196
  border: none;
192
197
  background: none;
193
198
  }
@@ -199,6 +204,12 @@
199
204
  .is-loading {
200
205
  color: transparent;
201
206
 
207
+ /* Keep text transparent when a loading button is hovered — needed because
208
+ the hover state now sets `color` as a property declaration. */
209
+ &:not([data-disabled]):is(:hover, :active, :focus) {
210
+ color: transparent;
211
+ }
212
+
202
213
  * {
203
214
  opacity: 0;
204
215
  }
@@ -0,0 +1,22 @@
1
+ import { Dialog as _Dialog } from '@base-ui/react/dialog';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import { Button } from '../button';
4
+ import type { ActionProps } from './types';
5
+
6
+ /**
7
+ * Renders a button that closes the dialog when clicked.
8
+ * Accepts all Button component props for styling.
9
+ */
10
+ const Action = forwardRef< HTMLButtonElement, ActionProps >(
11
+ function DialogAction( { render, ...props }, ref ) {
12
+ return (
13
+ <_Dialog.Close
14
+ ref={ ref }
15
+ render={ <Button render={ render } /> }
16
+ { ...props }
17
+ />
18
+ );
19
+ }
20
+ );
21
+
22
+ export { Action };
@@ -0,0 +1,32 @@
1
+ import { Dialog as _Dialog } from '@base-ui/react/dialog';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import { __ } from '@wordpress/i18n';
4
+ import { close } from '@wordpress/icons';
5
+ import { IconButton } from '../icon-button';
6
+ import type { CloseIconProps } from './types';
7
+
8
+ /**
9
+ * Renders an icon button that closes the dialog when clicked.
10
+ * Provides a default close icon and accessible label.
11
+ */
12
+ const CloseIcon = forwardRef< HTMLButtonElement, CloseIconProps >(
13
+ function DialogCloseIcon( { icon, label, ...props }, ref ) {
14
+ return (
15
+ <_Dialog.Close
16
+ ref={ ref }
17
+ render={
18
+ <IconButton
19
+ variant="minimal"
20
+ size="compact"
21
+ tone="neutral"
22
+ { ...props }
23
+ icon={ icon ?? close }
24
+ label={ label ?? __( 'Close' ) }
25
+ />
26
+ }
27
+ />
28
+ );
29
+ }
30
+ );
31
+
32
+ export { CloseIcon };
@@ -0,0 +1,113 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ } from '@wordpress/element';
9
+
10
+ /**
11
+ * Whether validation is enabled. This is a build-time constant that allows
12
+ * bundlers to tree-shake all validation code in production builds.
13
+ */
14
+ const VALIDATION_ENABLED = process.env.NODE_ENV !== 'production';
15
+
16
+ type DialogValidationContextType = {
17
+ registerTitle: ( element: HTMLElement | null ) => void;
18
+ };
19
+
20
+ // Context is only created in development mode.
21
+ const DialogValidationContext = VALIDATION_ENABLED
22
+ ? createContext< DialogValidationContextType | null >( null )
23
+ : ( null as unknown as React.Context< DialogValidationContextType | null > );
24
+
25
+ /**
26
+ * Development-only hook to access the dialog validation context.
27
+ */
28
+ function useDialogValidationContextDev() {
29
+ return useContext( DialogValidationContext );
30
+ }
31
+
32
+ /**
33
+ * Production no-op hook.
34
+ */
35
+ function useDialogValidationContextProd() {
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Hook to access the dialog validation context.
41
+ * Returns null in production or if not within a Dialog.Popup.
42
+ */
43
+ export const useDialogValidationContext = VALIDATION_ENABLED
44
+ ? useDialogValidationContextDev
45
+ : useDialogValidationContextProd;
46
+
47
+ /**
48
+ * Development-only provider that tracks whether Dialog.Title is rendered.
49
+ */
50
+ function DialogValidationProviderDev( {
51
+ children,
52
+ }: {
53
+ children: React.ReactNode;
54
+ } ) {
55
+ const titleElementRef = useRef< HTMLElement | null >( null );
56
+
57
+ const registerTitle = useCallback( ( element: HTMLElement | null ) => {
58
+ titleElementRef.current = element;
59
+ }, [] );
60
+
61
+ const contextValue = useMemo(
62
+ () => ( { registerTitle } ),
63
+ [ registerTitle ]
64
+ );
65
+
66
+ // Validate that Dialog.Title is rendered with non-empty text content
67
+ useEffect( () => {
68
+ // useLayoutEffect in Title runs before this useEffect,
69
+ // so titleElementRef should already be set if Title is present
70
+ const titleElement = titleElementRef.current;
71
+
72
+ if ( ! titleElement ) {
73
+ throw new Error(
74
+ 'Dialog: Missing <Dialog.Title>. ' +
75
+ 'For accessibility, every dialog requires a title. ' +
76
+ 'If needed, the title can be visually hidden but must not be omitted.'
77
+ );
78
+ }
79
+
80
+ const textContent = titleElement.textContent?.trim();
81
+ if ( ! textContent ) {
82
+ throw new Error(
83
+ 'Dialog: <Dialog.Title> cannot be empty. ' +
84
+ 'Provide meaningful text content for the dialog title.'
85
+ );
86
+ }
87
+ }, [] );
88
+
89
+ return (
90
+ <DialogValidationContext.Provider value={ contextValue }>
91
+ { children }
92
+ </DialogValidationContext.Provider>
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Production no-op provider that just renders children.
98
+ */
99
+ function DialogValidationProviderProd( {
100
+ children,
101
+ }: {
102
+ children: React.ReactNode;
103
+ } ) {
104
+ return <>{ children }</>;
105
+ }
106
+
107
+ /**
108
+ * Provider component that validates Dialog.Title presence in development mode.
109
+ * In production, this component is a no-op and just renders children.
110
+ */
111
+ export const DialogValidationProvider = VALIDATION_ENABLED
112
+ ? DialogValidationProviderDev
113
+ : DialogValidationProviderProd;
@@ -0,0 +1,26 @@
1
+ import { mergeProps, useRender } from '@base-ui/react';
2
+ import clsx from 'clsx';
3
+ import { forwardRef } from '@wordpress/element';
4
+ import styles from './style.module.css';
5
+ import type { FooterProps } from './types';
6
+
7
+ /**
8
+ * Renders the footer section of the dialog, typically containing
9
+ * action buttons.
10
+ */
11
+ const Footer = forwardRef< HTMLDivElement, FooterProps >( function DialogFooter(
12
+ { className, render, ...props },
13
+ ref
14
+ ) {
15
+ const element = useRender( {
16
+ render,
17
+ ref,
18
+ props: mergeProps< 'div' >( props, {
19
+ className: clsx( styles.footer, className ),
20
+ } ),
21
+ } );
22
+
23
+ return element;
24
+ } );
25
+
26
+ export { Footer };
@@ -0,0 +1,26 @@
1
+ import { mergeProps, useRender } from '@base-ui/react';
2
+ import clsx from 'clsx';
3
+ import { forwardRef } from '@wordpress/element';
4
+ import styles from './style.module.css';
5
+ import type { HeaderProps } from './types';
6
+
7
+ /**
8
+ * Renders the header section of the dialog, typically containing
9
+ * the heading and close button.
10
+ */
11
+ const Header = forwardRef< HTMLDivElement, HeaderProps >( function DialogHeader(
12
+ { className, render, ...props },
13
+ ref
14
+ ) {
15
+ const element = useRender( {
16
+ render,
17
+ ref,
18
+ props: mergeProps< 'div' >( props, {
19
+ className: clsx( styles.header, className ),
20
+ } ),
21
+ } );
22
+
23
+ return element;
24
+ } );
25
+
26
+ export { Header };
@@ -0,0 +1,10 @@
1
+ import { Action } from './action';
2
+ import { CloseIcon } from './close-icon';
3
+ import { Footer } from './footer';
4
+ import { Header } from './header';
5
+ import { Popup } from './popup';
6
+ import { Root } from './root';
7
+ import { Title } from './title';
8
+ import { Trigger } from './trigger';
9
+
10
+ export { Action, CloseIcon, Footer, Header, Popup, Root, Title, Trigger };
@@ -0,0 +1,46 @@
1
+ import { Dialog as _Dialog } from '@base-ui/react/dialog';
2
+ import clsx from 'clsx';
3
+ import { forwardRef } from '@wordpress/element';
4
+ import {
5
+ type ThemeProvider as ThemeProviderType,
6
+ privateApis as themePrivateApis,
7
+ } from '@wordpress/theme';
8
+ import { unlock } from '../lock-unlock';
9
+ import { DialogValidationProvider } from './context';
10
+ import styles from './style.module.css';
11
+ import type { PopupProps } from './types';
12
+
13
+ const ThemeProvider: typeof ThemeProviderType =
14
+ unlock( themePrivateApis ).ThemeProvider;
15
+
16
+ /**
17
+ * Renders the dialog popup element that contains the dialog content.
18
+ * Uses a portal to render outside the DOM hierarchy.
19
+ */
20
+ const Popup = forwardRef< HTMLDivElement, PopupProps >( function DialogPopup(
21
+ { className, size = 'medium', children, ...props },
22
+ ref
23
+ ) {
24
+ return (
25
+ <_Dialog.Portal>
26
+ <_Dialog.Backdrop className={ styles.backdrop } />
27
+ <ThemeProvider>
28
+ <_Dialog.Popup
29
+ ref={ ref }
30
+ className={ clsx(
31
+ styles.popup,
32
+ className,
33
+ styles[ `is-${ size }` ]
34
+ ) }
35
+ { ...props }
36
+ >
37
+ <DialogValidationProvider>
38
+ { children }
39
+ </DialogValidationProvider>
40
+ </_Dialog.Popup>
41
+ </ThemeProvider>
42
+ </_Dialog.Portal>
43
+ );
44
+ } );
45
+
46
+ export { Popup };
@@ -0,0 +1,14 @@
1
+ import { Dialog as _Dialog } from '@base-ui/react/dialog';
2
+ import type { RootProps } from './types';
3
+
4
+ /**
5
+ * Groups the dialog trigger and popup.
6
+ *
7
+ * `Dialog` is a collection of React components that combine to render
8
+ * an ARIA-compliant dialog pattern.
9
+ */
10
+ function Root( props: RootProps ) {
11
+ return <_Dialog.Root { ...props } />;
12
+ }
13
+
14
+ export { Root };
@@ -0,0 +1,177 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useId, useState } from '@wordpress/element';
3
+ import type { ComponentProps } from 'react';
4
+ import * as Dialog from '../index';
5
+
6
+ const meta: Meta< typeof Dialog.Root > = {
7
+ title: 'Design System/Components/Dialog',
8
+ component: Dialog.Root,
9
+ subcomponents: {
10
+ 'Dialog.Trigger': Dialog.Trigger,
11
+ 'Dialog.Popup': Dialog.Popup,
12
+ 'Dialog.Header': Dialog.Header,
13
+ 'Dialog.Title': Dialog.Title,
14
+ 'Dialog.CloseIcon': Dialog.CloseIcon,
15
+ 'Dialog.Action': Dialog.Action,
16
+ 'Dialog.Footer': Dialog.Footer,
17
+ },
18
+ argTypes: {
19
+ modal: {
20
+ control: 'inline-radio',
21
+ options: [ true, false, 'trap-focus' ],
22
+ table: {
23
+ defaultValue: { summary: 'true' },
24
+ type: {
25
+ summary: 'boolean | "trap-focus"',
26
+ },
27
+ },
28
+ },
29
+ },
30
+ parameters: {
31
+ docs: {
32
+ description: {
33
+ component: `
34
+ Dialog is a popup that opens on top of the entire page. Every dialog must include a \`Dialog.Title\` component for accessibility — it serves as both the visible heading and the accessible label for the dialog.
35
+
36
+ When using the Dialog component, make sure to always include a visible close button, either \`Dialog.CloseIcon\` or a clear dismissing action button. If your dialog has a "Cancel" button in the footer, the close icon may be redundant and create confusion about what clicking "X" means.
37
+
38
+ Use \`Dialog.CloseIcon\` for informational dialogs where dismissing is safe and expected. For dialogs requiring explicit user choice (especially destructive actions), omit the close icon and rely on footer action buttons like "Cancel" and "Confirm" instead.
39
+ `,
40
+ },
41
+ },
42
+ },
43
+ };
44
+ export default meta;
45
+
46
+ type Story = StoryObj< typeof Dialog.Root >;
47
+
48
+ /**
49
+ * An informational dialog with a close icon, where there is no ambiguity on
50
+ * what happens when clicking the close icon.
51
+ */
52
+ export const _Default: Story = {
53
+ args: {
54
+ children: (
55
+ <>
56
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
57
+ <Dialog.Popup>
58
+ <Dialog.Header>
59
+ <Dialog.Title>Welcome</Dialog.Title>
60
+ <Dialog.CloseIcon />
61
+ </Dialog.Header>
62
+ <p>
63
+ This dialog demonstrates best practices for
64
+ informational dialogs. It includes a close icon because
65
+ dismissing it is safe and expected.
66
+ </p>
67
+ <Dialog.Footer>
68
+ <Dialog.Action>Got it</Dialog.Action>
69
+ </Dialog.Footer>
70
+ </Dialog.Popup>
71
+ </>
72
+ ),
73
+ },
74
+ };
75
+
76
+ /**
77
+ * A confirmation dialog that intentionally omits the close icon. The user
78
+ * must explicitly choose "Cancel" or "Confirm" to make their intent clear,
79
+ * since it is not obvious what would happen when clicking a close icon.
80
+ */
81
+ export const ConfirmDialog: Story = {
82
+ args: {
83
+ children: (
84
+ <>
85
+ <Dialog.Trigger>Confirm Action</Dialog.Trigger>
86
+ <Dialog.Popup>
87
+ <Dialog.Header>
88
+ <Dialog.Title>Confirm Action</Dialog.Title>
89
+ </Dialog.Header>
90
+ <p>
91
+ Are you sure you want to proceed? This action cannot be
92
+ undone.
93
+ </p>
94
+ <Dialog.Footer>
95
+ <Dialog.Action variant="outline">Cancel</Dialog.Action>
96
+ <Dialog.Action>Confirm</Dialog.Action>
97
+ </Dialog.Footer>
98
+ </Dialog.Popup>
99
+ </>
100
+ ),
101
+ },
102
+ };
103
+
104
+ const ALL_SIZES = [ 'small', 'medium', 'large', 'stretch', 'full' ] as const;
105
+
106
+ function SizeSelector( {
107
+ value,
108
+ onChange,
109
+ }: {
110
+ value: ComponentProps< typeof Dialog.Popup >[ 'size' ];
111
+ onChange: ( size: ComponentProps< typeof Dialog.Popup >[ 'size' ] ) => void;
112
+ } ) {
113
+ const selectId = useId();
114
+ return (
115
+ <div style={ { display: 'flex', gap: 8, alignItems: 'center' } }>
116
+ <label htmlFor={ selectId }>Dialog size preset</label>
117
+ <select
118
+ id={ selectId }
119
+ value={ value }
120
+ onChange={ ( e ) =>
121
+ onChange(
122
+ e.target.value as ComponentProps<
123
+ typeof Dialog.Popup
124
+ >[ 'size' ]
125
+ )
126
+ }
127
+ >
128
+ { ALL_SIZES.map( ( s ) => (
129
+ <option key={ s } value={ s }>
130
+ { s }
131
+ { s === 'medium' ? ' (default)' : '' }
132
+ </option>
133
+ ) ) }
134
+ </select>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function SizePlaygroundContent() {
140
+ const [ size, setSize ] =
141
+ useState< ComponentProps< typeof Dialog.Popup >[ 'size' ] >( 'medium' );
142
+ return (
143
+ <>
144
+ <div
145
+ style={ {
146
+ display: 'flex',
147
+ flexDirection: 'column',
148
+ gap: 16,
149
+ alignItems: 'start',
150
+ } }
151
+ >
152
+ <SizeSelector value={ size } onChange={ setSize } />
153
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
154
+ </div>
155
+ <Dialog.Popup size={ size }>
156
+ <Dialog.Header>
157
+ <Dialog.Title>Size Playground</Dialog.Title>
158
+ <Dialog.CloseIcon />
159
+ </Dialog.Header>
160
+ <SizeSelector value={ size } onChange={ setSize } />
161
+ <p>
162
+ Use the dropdown above (or outside the dialog) to change the
163
+ popup size. Both controls stay in sync.
164
+ </p>
165
+ <Dialog.Footer>
166
+ <Dialog.Action>Got it</Dialog.Action>
167
+ </Dialog.Footer>
168
+ </Dialog.Popup>
169
+ </>
170
+ );
171
+ }
172
+
173
+ export const AllSizes: Story = {
174
+ args: {
175
+ children: <SizePlaygroundContent />,
176
+ },
177
+ };
@@ -0,0 +1,114 @@
1
+ @layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;
2
+
3
+ @layer wp-ui-components {
4
+ .backdrop {
5
+ position: fixed;
6
+ inset: 0;
7
+ background-color: rgba(0, 0, 0, 0.35);
8
+
9
+ &[data-starting-style],
10
+ &[data-ending-style] {
11
+ opacity: 0;
12
+ }
13
+
14
+ &[data-open] {
15
+ opacity: 1;
16
+ }
17
+
18
+ @media not (prefers-reduced-motion) {
19
+ transition: opacity 0.2s ease-out;
20
+ }
21
+ }
22
+
23
+ .popup {
24
+ position: fixed;
25
+ top: 50%;
26
+ /* stylelint-disable-next-line plugin/use-logical-properties-and-values -- Physical centering technique used with transform: translate(-50%, -50%) */
27
+ left: 50%;
28
+ transform: translate(-50%, -50%);
29
+ box-sizing: border-box;
30
+ min-width: 320px;
31
+ width: calc(100vw - 2 * var(--wpds-dimension-padding-2xl));
32
+ max-height: calc(100vh - 2 * var(--wpds-dimension-padding-2xl));
33
+ padding: var(--wpds-dimension-padding-2xl);
34
+ background-color: var(--wpds-color-bg-surface-neutral-strong);
35
+ border-radius: var(--wpds-border-radius-lg);
36
+ box-shadow: var(--wpds-elevation-lg);
37
+ overflow: auto;
38
+ font-family: var(--wpds-font-family-body);
39
+ font-size: var(--wpds-font-size-md);
40
+ line-height: var(--wpds-font-line-height-md);
41
+ color: var(--wpds-color-fg-content-neutral);
42
+
43
+ &[data-starting-style],
44
+ &[data-ending-style] {
45
+ opacity: 0;
46
+ transform: translate(-50%, -50%) scale(0.9);
47
+ }
48
+
49
+ @media not (prefers-reduced-motion) {
50
+ transition:
51
+ opacity 0.2s cubic-bezier(1, 0, 0.2, 1),
52
+ transform 0.2s cubic-bezier(1, 0, 0.2, 1);
53
+
54
+ &[data-open] {
55
+ transition:
56
+ opacity 0.2s cubic-bezier(0.29, 0, 0, 1),
57
+ transform 0.2s cubic-bezier(0.29, 0, 0, 1);
58
+ }
59
+ }
60
+
61
+ @media (min-width: 480px) {
62
+ min-width: 384px;
63
+ }
64
+
65
+ &.is-small {
66
+ max-width: 384px;
67
+ }
68
+
69
+ &.is-medium {
70
+ max-width: 512px;
71
+ }
72
+
73
+ &.is-large {
74
+ max-width: 840px;
75
+ }
76
+
77
+ &.is-stretch {
78
+ /* The dialog stretches to fill available width. */
79
+ max-width: none;
80
+ }
81
+
82
+ &.is-full {
83
+ /*
84
+ * Force full height (full width is already in default styles).
85
+ * The max-{width,height} properties will make sure some padding is shown.
86
+ */
87
+ height: 100vh;
88
+ }
89
+ }
90
+
91
+ .header {
92
+ display: flex;
93
+ justify-content: space-between;
94
+ align-items: center;
95
+ margin-bottom: var(--wpds-dimension-gap-lg);
96
+ }
97
+
98
+ .title {
99
+ margin: 0;
100
+ font-size: var(--wpds-font-size-xl);
101
+ font-weight: var(--wpds-font-weight-medium);
102
+ line-height: var(--wpds-font-line-height-xl);
103
+ color: var(--wpds-color-fg-content-neutral);
104
+ }
105
+
106
+ .footer {
107
+ display: flex;
108
+ justify-content: flex-end;
109
+ align-items: center;
110
+ gap: var(--wpds-dimension-gap-sm);
111
+ margin-top: var(--wpds-dimension-gap-lg);
112
+ padding-top: var(--wpds-dimension-padding-lg);
113
+ }
114
+ }