@wordpress/ui 0.10.0 → 0.11.1-next.v.202604091042.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 (245) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/CONTRIBUTING.md +25 -0
  3. package/README.md +22 -2
  4. package/build/alert-dialog/context.cjs +6 -1
  5. package/build/alert-dialog/context.cjs.map +2 -2
  6. package/build/alert-dialog/popup.cjs +105 -33
  7. package/build/alert-dialog/popup.cjs.map +4 -4
  8. package/build/alert-dialog/root.cjs +106 -6
  9. package/build/alert-dialog/root.cjs.map +2 -2
  10. package/build/alert-dialog/trigger.cjs +4 -14
  11. package/build/alert-dialog/trigger.cjs.map +3 -3
  12. package/build/alert-dialog/types.cjs.map +1 -1
  13. package/build/button/button.cjs +16 -6
  14. package/build/button/button.cjs.map +3 -3
  15. package/build/card/content.cjs +3 -3
  16. package/build/card/content.cjs.map +1 -1
  17. package/build/card/full-bleed.cjs +3 -3
  18. package/build/card/full-bleed.cjs.map +1 -1
  19. package/build/card/header.cjs +3 -3
  20. package/build/card/header.cjs.map +1 -1
  21. package/build/card/root.cjs +3 -3
  22. package/build/card/root.cjs.map +1 -1
  23. package/build/card/title.cjs +3 -3
  24. package/build/card/title.cjs.map +1 -1
  25. package/build/collapsible-card/header.cjs +3 -3
  26. package/build/collapsible-card/header.cjs.map +2 -2
  27. package/build/empty-state/title.cjs.map +2 -2
  28. package/build/form/primitives/field/description.cjs +17 -4
  29. package/build/form/primitives/field/description.cjs.map +3 -3
  30. package/build/form/primitives/field/details.cjs +3 -3
  31. package/build/form/primitives/field/details.cjs.map +2 -2
  32. package/build/form/primitives/field/label.cjs +3 -3
  33. package/build/form/primitives/field/label.cjs.map +2 -2
  34. package/build/form/primitives/fieldset/description.cjs +20 -4
  35. package/build/form/primitives/fieldset/description.cjs.map +3 -3
  36. package/build/form/primitives/fieldset/details.cjs +3 -3
  37. package/build/form/primitives/fieldset/details.cjs.map +2 -2
  38. package/build/form/primitives/fieldset/legend.cjs +3 -3
  39. package/build/form/primitives/fieldset/legend.cjs.map +2 -2
  40. package/build/form/primitives/input/input.cjs +23 -7
  41. package/build/form/primitives/input/input.cjs.map +3 -3
  42. package/build/form/primitives/input-layout/input-layout.cjs +10 -0
  43. package/build/form/primitives/input-layout/input-layout.cjs.map +3 -3
  44. package/build/form/primitives/select/trigger.cjs +3 -3
  45. package/build/form/primitives/select/trigger.cjs.map +2 -2
  46. package/build/form/primitives/textarea/textarea.cjs +20 -1
  47. package/build/form/primitives/textarea/textarea.cjs.map +3 -3
  48. package/build/index.cjs +3 -0
  49. package/build/index.cjs.map +2 -2
  50. package/build/link/link.cjs +16 -6
  51. package/build/link/link.cjs.map +3 -3
  52. package/build/popover/arrow.cjs +94 -0
  53. package/build/popover/arrow.cjs.map +7 -0
  54. package/build/popover/close.cjs +45 -0
  55. package/build/popover/close.cjs.map +7 -0
  56. package/build/popover/context.cjs +76 -0
  57. package/build/popover/context.cjs.map +7 -0
  58. package/build/popover/description.cjs +70 -0
  59. package/build/popover/description.cjs.map +7 -0
  60. package/build/popover/index.cjs +49 -0
  61. package/build/popover/index.cjs.map +7 -0
  62. package/build/popover/popup.cjs +138 -0
  63. package/build/popover/popup.cjs.map +7 -0
  64. package/build/popover/root.cjs +35 -0
  65. package/build/popover/root.cjs.map +7 -0
  66. package/build/popover/title.cjs +56 -0
  67. package/build/popover/title.cjs.map +7 -0
  68. package/build/popover/trigger.cjs +38 -0
  69. package/build/popover/trigger.cjs.map +7 -0
  70. package/build/popover/types.cjs +19 -0
  71. package/build/popover/types.cjs.map +7 -0
  72. package/build/text/text.cjs +20 -5
  73. package/build/text/text.cjs.map +3 -3
  74. package/build/utils/use-deprioritized-initial-focus.cjs.map +2 -2
  75. package/build-module/alert-dialog/context.mjs +6 -1
  76. package/build-module/alert-dialog/context.mjs.map +2 -2
  77. package/build-module/alert-dialog/popup.mjs +107 -33
  78. package/build-module/alert-dialog/popup.mjs.map +4 -4
  79. package/build-module/alert-dialog/root.mjs +113 -7
  80. package/build-module/alert-dialog/root.mjs.map +2 -2
  81. package/build-module/alert-dialog/trigger.mjs +4 -4
  82. package/build-module/alert-dialog/trigger.mjs.map +3 -3
  83. package/build-module/button/button.mjs +16 -6
  84. package/build-module/button/button.mjs.map +3 -3
  85. package/build-module/card/content.mjs +3 -3
  86. package/build-module/card/content.mjs.map +1 -1
  87. package/build-module/card/full-bleed.mjs +3 -3
  88. package/build-module/card/full-bleed.mjs.map +1 -1
  89. package/build-module/card/header.mjs +3 -3
  90. package/build-module/card/header.mjs.map +1 -1
  91. package/build-module/card/root.mjs +3 -3
  92. package/build-module/card/root.mjs.map +1 -1
  93. package/build-module/card/title.mjs +3 -3
  94. package/build-module/card/title.mjs.map +1 -1
  95. package/build-module/collapsible-card/header.mjs +3 -3
  96. package/build-module/collapsible-card/header.mjs.map +2 -2
  97. package/build-module/empty-state/title.mjs.map +2 -2
  98. package/build-module/form/primitives/field/description.mjs +17 -4
  99. package/build-module/form/primitives/field/description.mjs.map +3 -3
  100. package/build-module/form/primitives/field/details.mjs +3 -3
  101. package/build-module/form/primitives/field/details.mjs.map +2 -2
  102. package/build-module/form/primitives/field/label.mjs +3 -3
  103. package/build-module/form/primitives/field/label.mjs.map +2 -2
  104. package/build-module/form/primitives/fieldset/description.mjs +20 -4
  105. package/build-module/form/primitives/fieldset/description.mjs.map +3 -3
  106. package/build-module/form/primitives/fieldset/details.mjs +3 -3
  107. package/build-module/form/primitives/fieldset/details.mjs.map +2 -2
  108. package/build-module/form/primitives/fieldset/legend.mjs +3 -3
  109. package/build-module/form/primitives/fieldset/legend.mjs.map +2 -2
  110. package/build-module/form/primitives/input/input.mjs +23 -7
  111. package/build-module/form/primitives/input/input.mjs.map +3 -3
  112. package/build-module/form/primitives/input-layout/input-layout.mjs +10 -0
  113. package/build-module/form/primitives/input-layout/input-layout.mjs.map +3 -3
  114. package/build-module/form/primitives/select/trigger.mjs +3 -3
  115. package/build-module/form/primitives/select/trigger.mjs.map +2 -2
  116. package/build-module/form/primitives/textarea/textarea.mjs +20 -1
  117. package/build-module/form/primitives/textarea/textarea.mjs.map +3 -3
  118. package/build-module/index.mjs +2 -0
  119. package/build-module/index.mjs.map +2 -2
  120. package/build-module/link/link.mjs +16 -6
  121. package/build-module/link/link.mjs.map +3 -3
  122. package/build-module/popover/arrow.mjs +59 -0
  123. package/build-module/popover/arrow.mjs.map +7 -0
  124. package/build-module/popover/close.mjs +20 -0
  125. package/build-module/popover/close.mjs.map +7 -0
  126. package/build-module/popover/context.mjs +57 -0
  127. package/build-module/popover/context.mjs.map +7 -0
  128. package/build-module/popover/description.mjs +35 -0
  129. package/build-module/popover/description.mjs.map +7 -0
  130. package/build-module/popover/index.mjs +18 -0
  131. package/build-module/popover/index.mjs.map +7 -0
  132. package/build-module/popover/popup.mjs +105 -0
  133. package/build-module/popover/popup.mjs.map +7 -0
  134. package/build-module/popover/root.mjs +10 -0
  135. package/build-module/popover/root.mjs.map +7 -0
  136. package/build-module/popover/title.mjs +31 -0
  137. package/build-module/popover/title.mjs.map +7 -0
  138. package/build-module/popover/trigger.mjs +13 -0
  139. package/build-module/popover/trigger.mjs.map +7 -0
  140. package/build-module/popover/types.mjs +1 -0
  141. package/build-module/popover/types.mjs.map +7 -0
  142. package/build-module/text/text.mjs +20 -5
  143. package/build-module/text/text.mjs.map +3 -3
  144. package/build-module/utils/use-deprioritized-initial-focus.mjs.map +2 -2
  145. package/build-types/alert-dialog/context.d.ts +6 -3
  146. package/build-types/alert-dialog/context.d.ts.map +1 -1
  147. package/build-types/alert-dialog/popup.d.ts.map +1 -1
  148. package/build-types/alert-dialog/root.d.ts +2 -8
  149. package/build-types/alert-dialog/root.d.ts.map +1 -1
  150. package/build-types/alert-dialog/stories/index.story.d.ts +18 -6
  151. package/build-types/alert-dialog/stories/index.story.d.ts.map +1 -1
  152. package/build-types/alert-dialog/trigger.d.ts +2 -1
  153. package/build-types/alert-dialog/trigger.d.ts.map +1 -1
  154. package/build-types/alert-dialog/types.d.ts +57 -26
  155. package/build-types/alert-dialog/types.d.ts.map +1 -1
  156. package/build-types/button/button.d.ts.map +1 -1
  157. package/build-types/card/stories/index.story.d.ts.map +1 -1
  158. package/build-types/empty-state/title.d.ts.map +1 -1
  159. package/build-types/form/primitives/field/description.d.ts.map +1 -1
  160. package/build-types/form/primitives/fieldset/description.d.ts.map +1 -1
  161. package/build-types/form/primitives/input/input.d.ts.map +1 -1
  162. package/build-types/form/primitives/input-layout/input-layout.d.ts.map +1 -1
  163. package/build-types/form/primitives/textarea/textarea.d.ts.map +1 -1
  164. package/build-types/form/stories/shared.d.ts.map +1 -1
  165. package/build-types/index.d.ts +1 -0
  166. package/build-types/index.d.ts.map +1 -1
  167. package/build-types/link/link.d.ts.map +1 -1
  168. package/build-types/popover/arrow.d.ts +10 -0
  169. package/build-types/popover/arrow.d.ts.map +1 -0
  170. package/build-types/popover/close.d.ts +11 -0
  171. package/build-types/popover/close.d.ts.map +1 -0
  172. package/build-types/popover/context.d.ts +22 -0
  173. package/build-types/popover/context.d.ts.map +1 -0
  174. package/build-types/popover/description.d.ts +10 -0
  175. package/build-types/popover/description.d.ts.map +1 -0
  176. package/build-types/popover/index.d.ts +9 -0
  177. package/build-types/popover/index.d.ts.map +1 -0
  178. package/build-types/popover/popup.d.ts +11 -0
  179. package/build-types/popover/popup.d.ts.map +1 -0
  180. package/build-types/popover/root.d.ts +37 -0
  181. package/build-types/popover/root.d.ts.map +1 -0
  182. package/build-types/popover/stories/index.story.d.ts +211 -0
  183. package/build-types/popover/stories/index.story.d.ts.map +1 -0
  184. package/build-types/popover/stories/utils.d.ts +25 -0
  185. package/build-types/popover/stories/utils.d.ts.map +1 -0
  186. package/build-types/popover/test/index.test.d.ts +2 -0
  187. package/build-types/popover/test/index.test.d.ts.map +1 -0
  188. package/build-types/popover/title.d.ts +20 -0
  189. package/build-types/popover/title.d.ts.map +1 -0
  190. package/build-types/popover/trigger.d.ts +10 -0
  191. package/build-types/popover/trigger.d.ts.map +1 -0
  192. package/build-types/popover/types.d.ts +83 -0
  193. package/build-types/popover/types.d.ts.map +1 -0
  194. package/build-types/text/stories/index.story.d.ts +4 -0
  195. package/build-types/text/stories/index.story.d.ts.map +1 -1
  196. package/build-types/text/text.d.ts.map +1 -1
  197. package/build-types/utils/use-deprioritized-initial-focus.d.ts +6 -5
  198. package/build-types/utils/use-deprioritized-initial-focus.d.ts.map +1 -1
  199. package/package.json +11 -11
  200. package/src/alert-dialog/context.tsx +12 -4
  201. package/src/alert-dialog/popup.tsx +91 -33
  202. package/src/alert-dialog/root.tsx +191 -13
  203. package/src/alert-dialog/stories/index.story.tsx +116 -65
  204. package/src/alert-dialog/style.module.css +11 -0
  205. package/src/alert-dialog/test/index.test.tsx +1265 -347
  206. package/src/alert-dialog/trigger.tsx +2 -2
  207. package/src/alert-dialog/types.ts +59 -28
  208. package/src/button/button.tsx +2 -0
  209. package/src/button/style.module.css +4 -0
  210. package/src/card/stories/index.story.tsx +0 -1
  211. package/src/card/style.module.css +1 -1
  212. package/src/card/test/index.test.tsx +0 -1
  213. package/src/empty-state/title.tsx +0 -1
  214. package/src/form/primitives/field/description.tsx +6 -1
  215. package/src/form/primitives/fieldset/description.tsx +9 -1
  216. package/src/form/primitives/input/input.tsx +6 -1
  217. package/src/form/primitives/input/style.module.css +4 -0
  218. package/src/form/primitives/input-layout/input-layout.tsx +2 -0
  219. package/src/form/primitives/textarea/textarea.tsx +10 -1
  220. package/src/form/stories/shared.tsx +4 -2
  221. package/src/index.ts +1 -0
  222. package/src/link/link.tsx +2 -0
  223. package/src/link/style.module.css +10 -0
  224. package/src/popover/arrow.tsx +49 -0
  225. package/src/popover/close.tsx +24 -0
  226. package/src/popover/context.tsx +100 -0
  227. package/src/popover/description.tsx +34 -0
  228. package/src/popover/index.ts +9 -0
  229. package/src/popover/popup.tsx +106 -0
  230. package/src/popover/root.tsx +41 -0
  231. package/src/popover/stories/index.story.tsx +1315 -0
  232. package/src/popover/stories/utils.tsx +91 -0
  233. package/src/popover/style.module.css +64 -0
  234. package/src/popover/test/index.test.tsx +727 -0
  235. package/src/popover/title.tsx +50 -0
  236. package/src/popover/trigger.tsx +17 -0
  237. package/src/popover/types.ts +113 -0
  238. package/src/text/stories/index.story.tsx +4 -2
  239. package/src/text/style.module.css +26 -0
  240. package/src/text/test/index.test.tsx +1 -4
  241. package/src/text/text.tsx +8 -1
  242. package/src/utils/css/field.module.css +4 -1
  243. package/src/utils/css/focus.module.css +7 -5
  244. package/src/utils/css/global-css-defense.module.css +117 -0
  245. package/src/utils/use-deprioritized-initial-focus.ts +5 -4
@@ -1,6 +1,6 @@
1
+ import { AlertDialog as _AlertDialog } from '@base-ui/react/alert-dialog';
1
2
  import { forwardRef } from '@wordpress/element';
2
3
 
3
- import * as Dialog from '../dialog';
4
4
  import type { TriggerProps } from './types';
5
5
 
6
6
  /**
@@ -8,7 +8,7 @@ import type { TriggerProps } from './types';
8
8
  */
9
9
  const Trigger = forwardRef< HTMLButtonElement, TriggerProps >(
10
10
  function AlertDialogTrigger( props, ref ) {
11
- return <Dialog.Trigger ref={ ref } { ...props } />;
11
+ return <_AlertDialog.Trigger ref={ ref } { ...props } />;
12
12
  }
13
13
  );
14
14
 
@@ -1,7 +1,18 @@
1
1
  import type { AlertDialog as _AlertDialog } from '@base-ui/react/alert-dialog';
2
2
  import type { ReactNode } from 'react';
3
3
 
4
- import type { TriggerProps as DialogTriggerProps } from '../dialog/types';
4
+ import type { ComponentProps } from '../utils/types';
5
+
6
+ /**
7
+ * The return type of `onConfirm`. Return `void` (or nothing) to auto-close
8
+ * the dialog after the confirm handler completes. Return `{ close: false }`
9
+ * to keep the dialog open (e.g. for validation errors).
10
+ *
11
+ * Return `{ error: '...' }` to display a built-in error message below the
12
+ * action buttons. When `error` is provided, the dialog stays open
13
+ * regardless of the `close` value.
14
+ */
15
+ export type ConfirmResult = void | { close?: boolean; error?: string };
5
16
 
6
17
  export interface RootProps
7
18
  extends Pick<
@@ -14,6 +25,43 @@ export interface RootProps
14
25
  */
15
26
  children: ReactNode;
16
27
 
28
+ /**
29
+ * Callback fired when the user confirms the action.
30
+ *
31
+ * - Synchronous handlers: the dialog closes immediately after the
32
+ * handler returns.
33
+ * - Async handlers: the dialog enters a "pending" state (buttons
34
+ * disabled, spinner shown on the confirm button) until the promise
35
+ * settles.
36
+ *
37
+ * Return `{ close: false }` to keep the dialog open after the handler
38
+ * completes (e.g. for server-side validation). Return `void` or
39
+ * `{ close: true }` to close the dialog (the default).
40
+ *
41
+ * Return `{ error: '...' }` to show a built-in error message below
42
+ * the action buttons. The dialog stays open regardless of the `close`
43
+ * value. The error is announced to screen readers and is automatically
44
+ * cleared on the next confirm attempt or when the dialog reopens.
45
+ *
46
+ * If the promise rejects (or the handler throws) without returning an
47
+ * `error`, the dialog stays open and returns to idle without showing
48
+ * a visible error message. The error is logged to the console.
49
+ * To show a user-facing message on failure, catch the error and
50
+ * return `{ close: false, error: '...' }`.
51
+ */
52
+ onConfirm?: () => ConfirmResult | Promise< ConfirmResult >;
53
+ }
54
+
55
+ export interface TriggerProps extends ComponentProps< 'button' > {
56
+ /**
57
+ * The content to be rendered inside the component.
58
+ */
59
+ children?: ReactNode;
60
+ }
61
+
62
+ export interface PopupProps
63
+ extends ComponentProps< 'div' >,
64
+ Pick< _AlertDialog.Popup.Props, 'initialFocus' | 'finalFocus' > {
17
65
  /**
18
66
  * The semantic intent of the dialog, which determines its styling.
19
67
  *
@@ -28,26 +76,27 @@ export interface RootProps
28
76
  * @default 'default'
29
77
  */
30
78
  intent?: 'default' | 'irreversible';
31
- }
32
79
 
33
- export type TriggerProps = DialogTriggerProps;
34
-
35
- export interface PopupProps {
36
80
  /**
37
81
  * The title displayed in the dialog header. This serves as both the
38
- * visible heading and the accessible label for the dialog.
82
+ * visible heading and the accessible label (`aria-labelledby`) for the
83
+ * dialog. Must be a plain string to ensure a predictable accessible name.
39
84
  */
40
85
  title: string;
41
86
 
42
87
  /**
43
- * The message content displayed in the dialog body.
88
+ * An optional description displayed below the title. Rendered using
89
+ * Base UI's `AlertDialog.Description` for proper `aria-describedby`
90
+ * association with the dialog. Must be a plain string to ensure a
91
+ * predictable accessible description.
44
92
  */
45
- children: ReactNode;
93
+ description?: string;
46
94
 
47
95
  /**
48
- * Callback fired when the user confirms the action.
96
+ * Optional body content displayed between the description and the
97
+ * action buttons. Use for supplementary details or form fields.
49
98
  */
50
- onConfirm: () => void;
99
+ children?: ReactNode;
51
100
 
52
101
  /**
53
102
  * Custom text for the confirm button.
@@ -62,22 +111,4 @@ export interface PopupProps {
62
111
  * @default 'Cancel'
63
112
  */
64
113
  cancelButtonText?: string;
65
-
66
- /**
67
- * Whether the confirm action is in a loading state (e.g. an async
68
- * operation is in progress). When `true`, the confirm button shows a
69
- * spinner and the cancel button is disabled.
70
- *
71
- * **Important:** Passing this prop — even as `false` — opts into
72
- * manual-close mode: the confirm button will no longer auto-close the
73
- * dialog. The consumer is responsible for setting `open={false}` when
74
- * the operation completes. Omit the prop entirely for the default
75
- * auto-close-on-confirm behavior.
76
- *
77
- * To implement an async confirm flow, use controlled mode
78
- * (`open` / `onOpenChange`) and manage the loading state externally:
79
- * prevent closing in `onOpenChange` while loading, and set
80
- * `open={false}` once the operation completes.
81
- */
82
- loading?: boolean;
83
114
  }
@@ -7,6 +7,7 @@ import { type ButtonProps } from './types';
7
7
  import styles from './style.module.css';
8
8
  import resetStyles from '../utils/css/resets.module.css';
9
9
  import focusStyles from '../utils/css/focus.module.css';
10
+ import defenseStyles from '../utils/css/global-css-defense.module.css';
10
11
 
11
12
  export const Button = forwardRef< HTMLButtonElement, ButtonProps >(
12
13
  function Button(
@@ -25,6 +26,7 @@ export const Button = forwardRef< HTMLButtonElement, ButtonProps >(
25
26
  ref
26
27
  ) {
27
28
  const mergedClassName = clsx(
29
+ defenseStyles.button,
28
30
  resetStyles[ 'box-sizing' ],
29
31
  focusStyles[ 'outset-ring--focus-except-active' ],
30
32
  variant !== 'unstyled' && styles.button,
@@ -28,6 +28,10 @@
28
28
  --wp-ui-button-border-color-active: var(--wp-ui-button-background-color-active);
29
29
  --wp-ui-button-border-color-disabled: var(--wp-ui-button-background-color-disabled);
30
30
 
31
+ --_gcd-button-font-family: var(--wpds-font-family-body);
32
+ --_gcd-button-font-size: var(--wp-ui-button-font-size);
33
+ --_gcd-button-font-weight: var(--wp-ui-button-font-weight);
34
+
31
35
  /* Styles */
32
36
  position: relative;
33
37
  display: inline-flex;
@@ -116,7 +116,6 @@ export const CustomSemantics: Story = {
116
116
  children: (
117
117
  <>
118
118
  <Card.Header>
119
- { /* eslint-disable-next-line jsx-a11y/heading-has-content -- content provided via render prop */ }
120
119
  <Card.Title render={ <h2 /> }>Section heading</Card.Title>
121
120
  </Card.Header>
122
121
  <Card.Content>
@@ -12,6 +12,7 @@
12
12
  border-radius: var(--wpds-border-radius-lg);
13
13
  overflow: clip;
14
14
  background-color: var(--wpds-color-bg-surface-neutral-strong);
15
+ color: var(--wpds-color-fg-content-neutral);
15
16
  }
16
17
 
17
18
  /* Padding is applied to the individual header/content elements to enable
@@ -40,6 +41,5 @@
40
41
 
41
42
  .title {
42
43
  margin: 0;
43
- color: var(--wpds-color-fg-content-neutral);
44
44
  }
45
45
  }
@@ -82,7 +82,6 @@ describe( 'Card', () => {
82
82
  render(
83
83
  <Card.Root>
84
84
  <Card.Header>
85
- { /* eslint-disable-next-line jsx-a11y/heading-has-content -- content provided via render prop */ }
86
85
  <Card.Title render={ <h2 /> }>Heading</Card.Title>
87
86
  </Card.Header>
88
87
  </Card.Root>
@@ -12,7 +12,6 @@ export const Title = forwardRef< HTMLHeadingElement, EmptyStateTitleProps >(
12
12
  return (
13
13
  <Text
14
14
  variant="heading-lg"
15
- // eslint-disable-next-line jsx-a11y/heading-has-content -- content provided via render prop
16
15
  render={ render ?? <h2 ref={ ref } { ...props } /> }
17
16
  className={ clsx( styles.title, className ) }
18
17
  >
@@ -1,6 +1,7 @@
1
1
  import clsx from 'clsx';
2
2
  import { Field as _Field } from '@base-ui/react/field';
3
3
  import { forwardRef } from '@wordpress/element';
4
+ import defenseStyles from '../../../utils/css/global-css-defense.module.css';
4
5
  import fieldStyles from '../../../utils/css/field.module.css';
5
6
  import type { FieldDescriptionProps } from './types';
6
7
 
@@ -11,7 +12,11 @@ export const Description = forwardRef<
11
12
  return (
12
13
  <_Field.Description
13
14
  ref={ ref }
14
- className={ clsx( fieldStyles.description, className ) }
15
+ className={ clsx(
16
+ defenseStyles.p,
17
+ fieldStyles.description,
18
+ className
19
+ ) }
15
20
  { ...restProps }
16
21
  />
17
22
  );
@@ -1,6 +1,7 @@
1
1
  import clsx from 'clsx';
2
2
  import { mergeProps, useRender } from '@base-ui/react';
3
3
  import { forwardRef, useEffect, useId } from '@wordpress/element';
4
+ import defenseStyles from '../../../utils/css/global-css-defense.module.css';
4
5
  import fieldStyles from '../../../utils/css/field.module.css';
5
6
  import { useFieldsetContext } from './context';
6
7
  import type { FieldsetDescriptionProps } from './types';
@@ -27,7 +28,14 @@ export const FieldsetDescription = forwardRef<
27
28
  render,
28
29
  ref,
29
30
  props: mergeProps< 'p' >(
30
- { className: clsx( fieldStyles.description, className ), id },
31
+ {
32
+ className: clsx(
33
+ defenseStyles.p,
34
+ fieldStyles.description,
35
+ className
36
+ ),
37
+ id,
38
+ },
31
39
  restProps
32
40
  ),
33
41
  } );
@@ -1,6 +1,7 @@
1
1
  import { Input as _Input } from '@base-ui/react/input';
2
2
  import clsx from 'clsx';
3
3
  import { forwardRef } from '@wordpress/element';
4
+ import defenseStyles from '../../../utils/css/global-css-defense.module.css';
4
5
  import focusStyles from '../../../utils/css/focus.module.css';
5
6
  import styles from './style.module.css';
6
7
  import type { InputProps } from './types';
@@ -22,7 +23,11 @@ export const Input = forwardRef< HTMLElement, InputProps >( function Input(
22
23
  prefix={ prefix }
23
24
  suffix={ suffix }
24
25
  >
25
- <_Input ref={ ref } className={ styles.input } { ...restProps } />
26
+ <_Input
27
+ ref={ ref }
28
+ className={ clsx( defenseStyles.input, styles.input ) }
29
+ { ...restProps }
30
+ />
26
31
  </InputLayout>
27
32
  );
28
33
  } );
@@ -2,6 +2,10 @@
2
2
 
3
3
  @layer wp-ui-components {
4
4
  .input {
5
+ --_gcd-input-padding:
6
+ var(--wp-ui-input-padding-block, 0)
7
+ var(--wp-ui-input-layout-padding-inline, 0);
8
+
5
9
  padding-block: var(--wp-ui-input-padding-block, 0);
6
10
  padding-inline: var(--wp-ui-input-layout-padding-inline, 0);
7
11
  width: 100%;
@@ -1,5 +1,6 @@
1
1
  import clsx from 'clsx';
2
2
  import { Children, forwardRef } from '@wordpress/element';
3
+ import defenseStyles from '../../../utils/css/global-css-defense.module.css';
3
4
  import resetStyles from '../../../utils/css/resets.module.css';
4
5
  import styles from './style.module.css';
5
6
  import type { InputLayoutProps } from './types';
@@ -26,6 +27,7 @@ export const InputLayout = forwardRef< HTMLDivElement, InputLayoutProps >(
26
27
  <div
27
28
  ref={ ref }
28
29
  className={ clsx(
30
+ defenseStyles.div,
29
31
  resetStyles[ 'box-sizing' ],
30
32
  styles[ 'input-layout' ],
31
33
  styles[ `is-size-${ size }` ],
@@ -4,6 +4,7 @@ import { cloneElement, forwardRef } from '@wordpress/element';
4
4
  import styles from './style.module.css';
5
5
  import type { TextareaProps } from './types';
6
6
  import { Input } from '../input';
7
+ import defenseStyles from '../../../utils/css/global-css-defense.module.css';
7
8
 
8
9
  const wrappedRender = (
9
10
  render: NonNullable< TextareaProps[ 'render' ] >,
@@ -39,7 +40,15 @@ export const Textarea = forwardRef< HTMLTextAreaElement, TextareaProps >(
39
40
  style={ style }
40
41
  render={ wrappedRender(
41
42
  render || ( ( props ) => <textarea { ...props } /> ),
42
- { className: styles.textarea, ref, rows, ...restProps }
43
+ {
44
+ className: clsx(
45
+ defenseStyles.textarea,
46
+ styles.textarea
47
+ ),
48
+ ref,
49
+ rows,
50
+ ...restProps,
51
+ }
43
52
  ) }
44
53
  value={ value }
45
54
  defaultValue={ defaultValue }
@@ -1,3 +1,5 @@
1
+ import { Link } from '../..';
2
+
1
3
  export const WITH_DETAILS_DESCRIPTION = `\
2
4
  To add rich content (such as links) to the description, use the \`details\` prop.
3
5
 
@@ -11,9 +13,9 @@ so the readout is not unnecessarily verbose for screen reader users.`;
11
13
  export const DETAILS_EXAMPLE = (
12
14
  <>
13
15
  Details can include{ ' ' }
14
- <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a">
16
+ <Link href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a">
15
17
  links to more information
16
- </a>{ ' ' }
18
+ </Link>{ ' ' }
17
19
  and other semantic elements.
18
20
  </>
19
21
  );
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from './icon';
11
11
  export * from './icon-button';
12
12
  export * from './link';
13
13
  export * as Notice from './notice';
14
+ export * as Popover from './popover';
14
15
  export * from './stack';
15
16
  export * as Tabs from './tabs';
16
17
  export * from './text';
package/src/link/link.tsx CHANGED
@@ -6,6 +6,7 @@ import { type LinkProps } from './types';
6
6
  import resetStyles from '../utils/css/resets.module.css';
7
7
  import focusStyles from '../utils/css/focus.module.css';
8
8
  import styles from './style.module.css';
9
+ import defenseStyles from '../utils/css/global-css-defense.module.css';
9
10
 
10
11
  /**
11
12
  * A styled anchor element with support for semantic color tones and an
@@ -39,6 +40,7 @@ export const Link = forwardRef< HTMLAnchorElement, LinkProps >( function Link(
39
40
  ref,
40
41
  props: mergeProps< 'a' >( props, {
41
42
  className: clsx(
43
+ defenseStyles.a,
42
44
  resetStyles[ 'box-sizing' ],
43
45
  focusStyles[ 'outset-ring--focus' ],
44
46
  variant !== 'unstyled' && styles.link,
@@ -9,28 +9,38 @@
9
9
  /* Brand tone */
10
10
  .is-brand,
11
11
  .is-brand:visited {
12
+ --_gcd-a-color: var(--wpds-color-fg-interactive-brand);
13
+
12
14
  color: var(--wpds-color-fg-interactive-brand);
13
15
  }
14
16
 
15
17
  .is-brand:hover,
16
18
  .is-brand:active {
19
+ --_gcd-a-color: var(--wpds-color-fg-interactive-brand-active);
20
+
17
21
  color: var(--wpds-color-fg-interactive-brand-active);
18
22
  }
19
23
 
20
24
  /* Neutral tone */
21
25
  .is-neutral,
22
26
  .is-neutral:visited {
27
+ --_gcd-a-color: var(--wpds-color-fg-interactive-neutral);
28
+
23
29
  color: var(--wpds-color-fg-interactive-neutral);
24
30
  text-decoration-color: var(--wpds-color-stroke-interactive-neutral);
25
31
  }
26
32
 
27
33
  .is-neutral:hover,
28
34
  .is-neutral:active {
35
+ --_gcd-a-color: var(--wpds-color-fg-interactive-neutral-active);
36
+
29
37
  color: var(--wpds-color-fg-interactive-neutral-active);
30
38
  }
31
39
 
32
40
  /* Unstyled variant */
33
41
  .is-unstyled {
42
+ --_gcd-a-color: inherit;
43
+
34
44
  color: inherit;
35
45
  text-decoration: none;
36
46
  }
@@ -0,0 +1,49 @@
1
+ import { Popover as _Popover } from '@base-ui/react/popover';
2
+ import clsx from 'clsx';
3
+ import { forwardRef } from '@wordpress/element';
4
+ import styles from './style.module.css';
5
+ import type { ArrowProps } from './types';
6
+
7
+ function DefaultArrowSvg( props: React.ComponentProps< 'svg' > ) {
8
+ return (
9
+ <svg
10
+ width="20"
11
+ height="10"
12
+ viewBox="0 0 20 10"
13
+ fill="none"
14
+ { ...props }
15
+ >
16
+ <path
17
+ d="M20 10H0V8h1.465a4 4 0 0 0 2.676-1.027l5.19-4.388c.378-.341.96-.341 1.338 0l5.19 4.388A4 4 0 0 0 18.535 8H20z"
18
+ className={ styles[ 'arrow-fill' ] }
19
+ />
20
+ <path
21
+ d="M10 3.097 4.81 7.486A5 5 0 0 1 1.465 8.77H0V8h1.465a4 4 0 0 0 2.676-1.027l5.19-4.388c.378-.341.96-.341 1.338 0l5.19 4.388A4 4 0 0 0 18.535 8H20v.77h-1.465a5 5 0 0 1-3.345-1.284z"
22
+ className={ styles[ 'arrow-stroke' ] }
23
+ />
24
+ </svg>
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Renders an optional arrow element that points toward the anchor.
30
+ *
31
+ * Must be placed inside `Popover.Popup`. The arrow automatically rotates
32
+ * to match the current placement side.
33
+ */
34
+ const Arrow = forwardRef< HTMLDivElement, ArrowProps >( function PopoverArrow(
35
+ { children, className, ...props },
36
+ ref
37
+ ) {
38
+ return (
39
+ <_Popover.Arrow
40
+ ref={ ref }
41
+ className={ clsx( styles.arrow, className ) }
42
+ { ...props }
43
+ >
44
+ { children ?? <DefaultArrowSvg /> }
45
+ </_Popover.Arrow>
46
+ );
47
+ } );
48
+
49
+ export { Arrow };
@@ -0,0 +1,24 @@
1
+ import { Popover as _Popover } from '@base-ui/react/popover';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import type { CloseProps } from './types';
4
+
5
+ /**
6
+ * Renders a button that closes the popover when clicked.
7
+ *
8
+ * When the popover opens, initial focus skips this button in favor of
9
+ * the first interactive element inside the popup (unless `initialFocus`
10
+ * on `Popover.Popup` overrides this behavior).
11
+ */
12
+ const Close = forwardRef< HTMLButtonElement, CloseProps >(
13
+ function PopoverClose( props, ref ) {
14
+ return (
15
+ <_Popover.Close
16
+ ref={ ref }
17
+ data-wp-ui-popover-close=""
18
+ { ...props }
19
+ />
20
+ );
21
+ }
22
+ );
23
+
24
+ export { Close };
@@ -0,0 +1,100 @@
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 PopoverValidationContextType = {
17
+ registerTitle: ( element: HTMLElement | null ) => void;
18
+ };
19
+
20
+ const PopoverValidationContext = VALIDATION_ENABLED
21
+ ? createContext< PopoverValidationContextType | null >( null )
22
+ : ( null as unknown as React.Context< PopoverValidationContextType | null > );
23
+
24
+ function usePopoverValidationContextDev() {
25
+ return useContext( PopoverValidationContext );
26
+ }
27
+
28
+ function usePopoverValidationContextProd() {
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * Hook to access the popover validation context.
34
+ * Returns null in production or if not within a Popover.Popup.
35
+ */
36
+ export const usePopoverValidationContext = VALIDATION_ENABLED
37
+ ? usePopoverValidationContextDev
38
+ : usePopoverValidationContextProd;
39
+
40
+ /**
41
+ * Development-only provider that tracks whether Popover.Title is rendered.
42
+ */
43
+ function PopoverValidationProviderDev( {
44
+ children,
45
+ }: {
46
+ children: React.ReactNode;
47
+ } ) {
48
+ const titleElementRef = useRef< HTMLElement | null >( null );
49
+
50
+ const registerTitle = useCallback( ( element: HTMLElement | null ) => {
51
+ titleElementRef.current = element;
52
+ }, [] );
53
+
54
+ const contextValue = useMemo(
55
+ () => ( { registerTitle } ),
56
+ [ registerTitle ]
57
+ );
58
+
59
+ useEffect( () => {
60
+ const titleElement = titleElementRef.current;
61
+
62
+ if ( ! titleElement ) {
63
+ throw new Error(
64
+ 'Popover: Missing <Popover.Title>. ' +
65
+ 'For accessibility, every popover requires a title. ' +
66
+ 'If needed, the title can be visually hidden but must not be omitted.'
67
+ );
68
+ }
69
+
70
+ const textContent = titleElement.textContent?.trim();
71
+ if ( ! textContent ) {
72
+ throw new Error(
73
+ 'Popover: <Popover.Title> cannot be empty. ' +
74
+ 'Provide meaningful text content for the popover title.'
75
+ );
76
+ }
77
+ }, [] );
78
+
79
+ return (
80
+ <PopoverValidationContext.Provider value={ contextValue }>
81
+ { children }
82
+ </PopoverValidationContext.Provider>
83
+ );
84
+ }
85
+
86
+ function PopoverValidationProviderProd( {
87
+ children,
88
+ }: {
89
+ children: React.ReactNode;
90
+ } ) {
91
+ return <>{ children }</>;
92
+ }
93
+
94
+ /**
95
+ * Provider component that validates Popover.Title presence in development mode.
96
+ * In production, this component is a no-op and just renders children.
97
+ */
98
+ export const PopoverValidationProvider = VALIDATION_ENABLED
99
+ ? PopoverValidationProviderDev
100
+ : PopoverValidationProviderProd;
@@ -0,0 +1,34 @@
1
+ import { Popover as _Popover } from '@base-ui/react/popover';
2
+ import clsx from 'clsx';
3
+ import { forwardRef } from '@wordpress/element';
4
+ import { Text } from '../text';
5
+ import styles from './style.module.css';
6
+ import type { DescriptionProps } from './types';
7
+
8
+ /**
9
+ * Renders an optional paragraph that describes the popover content.
10
+ *
11
+ * The rendered element is linked to the popup via `aria-describedby`.
12
+ * Uses the `body-md` text variant by default.
13
+ */
14
+ const Description = forwardRef< HTMLParagraphElement, DescriptionProps >(
15
+ function PopoverDescription(
16
+ { className, children, render, ...props },
17
+ ref
18
+ ) {
19
+ return (
20
+ <Text
21
+ ref={ ref }
22
+ variant="body-md"
23
+ render={
24
+ <_Popover.Description render={ render } { ...props } />
25
+ }
26
+ className={ clsx( styles.description, className ) }
27
+ >
28
+ { children }
29
+ </Text>
30
+ );
31
+ }
32
+ );
33
+
34
+ export { Description };
@@ -0,0 +1,9 @@
1
+ import { Arrow } from './arrow';
2
+ import { Close } from './close';
3
+ import { Description } from './description';
4
+ import { Popup } from './popup';
5
+ import { Root } from './root';
6
+ import { Title } from './title';
7
+ import { Trigger } from './trigger';
8
+
9
+ export { Arrow, Close, Description, Popup, Root, Title, Trigger };