@wordpress/ui 0.8.0 → 0.9.1-next.v.202603102151.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 +19 -0
  2. package/build/card/content.cjs +54 -0
  3. package/build/card/content.cjs.map +7 -0
  4. package/build/card/full-bleed.cjs +57 -0
  5. package/build/card/full-bleed.cjs.map +7 -0
  6. package/build/card/header.cjs +54 -0
  7. package/build/card/header.cjs.map +7 -0
  8. package/build/card/index.cjs +43 -0
  9. package/build/card/index.cjs.map +7 -0
  10. package/build/card/root.cjs +73 -0
  11. package/build/card/root.cjs.map +7 -0
  12. package/build/card/title.cjs +55 -0
  13. package/build/card/title.cjs.map +7 -0
  14. package/build/card/types.cjs +19 -0
  15. package/build/card/types.cjs.map +7 -0
  16. package/build/collapsible-card/content.cjs +56 -0
  17. package/build/collapsible-card/content.cjs.map +7 -0
  18. package/build/collapsible-card/header.cjs +104 -0
  19. package/build/collapsible-card/header.cjs.map +7 -0
  20. package/build/collapsible-card/index.cjs +37 -0
  21. package/build/collapsible-card/index.cjs.map +7 -0
  22. package/build/collapsible-card/root.cjs +56 -0
  23. package/build/collapsible-card/root.cjs.map +7 -0
  24. package/build/collapsible-card/types.cjs +19 -0
  25. package/build/collapsible-card/types.cjs.map +7 -0
  26. package/build/form/primitives/field/label.cjs +6 -1
  27. package/build/form/primitives/field/label.cjs.map +2 -2
  28. package/build/form/primitives/field/types.cjs.map +1 -1
  29. package/build/form/primitives/fieldset/legend.cjs +5 -1
  30. package/build/form/primitives/fieldset/legend.cjs.map +2 -2
  31. package/build/form/primitives/fieldset/types.cjs.map +1 -1
  32. package/build/index.cjs +10 -0
  33. package/build/index.cjs.map +2 -2
  34. package/build/link/index.cjs +31 -0
  35. package/build/link/index.cjs.map +7 -0
  36. package/build/link/link.cjs +125 -0
  37. package/build/link/link.cjs.map +7 -0
  38. package/build/link/types.cjs +19 -0
  39. package/build/link/types.cjs.map +7 -0
  40. package/build/notice/action-button.cjs +3 -3
  41. package/build/notice/action-button.cjs.map +2 -2
  42. package/build/notice/action-link.cjs +17 -18
  43. package/build/notice/action-link.cjs.map +2 -2
  44. package/build/notice/actions.cjs +3 -3
  45. package/build/notice/actions.cjs.map +2 -2
  46. package/build/notice/close-icon.cjs +3 -3
  47. package/build/notice/close-icon.cjs.map +2 -2
  48. package/build/notice/description.cjs +26 -15
  49. package/build/notice/description.cjs.map +3 -3
  50. package/build/notice/root.cjs +3 -3
  51. package/build/notice/root.cjs.map +2 -2
  52. package/build/notice/title.cjs +26 -12
  53. package/build/notice/title.cjs.map +3 -3
  54. package/build/notice/types.cjs.map +1 -1
  55. package/build/text/index.cjs +31 -0
  56. package/build/text/index.cjs.map +7 -0
  57. package/build/text/text.cjs +65 -0
  58. package/build/text/text.cjs.map +7 -0
  59. package/build/text/types.cjs +19 -0
  60. package/build/text/types.cjs.map +7 -0
  61. package/build/tooltip/popup.cjs +1 -1
  62. package/build/tooltip/popup.cjs.map +1 -1
  63. package/build/visually-hidden/visually-hidden.cjs +3 -3
  64. package/build/visually-hidden/visually-hidden.cjs.map +2 -2
  65. package/build-module/card/content.mjs +29 -0
  66. package/build-module/card/content.mjs.map +7 -0
  67. package/build-module/card/full-bleed.mjs +32 -0
  68. package/build-module/card/full-bleed.mjs.map +7 -0
  69. package/build-module/card/header.mjs +29 -0
  70. package/build-module/card/header.mjs.map +7 -0
  71. package/build-module/card/index.mjs +14 -0
  72. package/build-module/card/index.mjs.map +7 -0
  73. package/build-module/card/root.mjs +38 -0
  74. package/build-module/card/root.mjs.map +7 -0
  75. package/build-module/card/title.mjs +30 -0
  76. package/build-module/card/title.mjs.map +7 -0
  77. package/build-module/card/types.mjs +1 -0
  78. package/build-module/card/types.mjs.map +7 -0
  79. package/build-module/collapsible-card/content.mjs +21 -0
  80. package/build-module/collapsible-card/content.mjs.map +7 -0
  81. package/build-module/collapsible-card/header.mjs +69 -0
  82. package/build-module/collapsible-card/header.mjs.map +7 -0
  83. package/build-module/collapsible-card/index.mjs +10 -0
  84. package/build-module/collapsible-card/index.mjs.map +7 -0
  85. package/build-module/collapsible-card/root.mjs +21 -0
  86. package/build-module/collapsible-card/root.mjs.map +7 -0
  87. package/build-module/collapsible-card/types.mjs +1 -0
  88. package/build-module/collapsible-card/types.mjs.map +7 -0
  89. package/build-module/form/primitives/field/label.mjs +6 -1
  90. package/build-module/form/primitives/field/label.mjs.map +2 -2
  91. package/build-module/form/primitives/fieldset/legend.mjs +5 -1
  92. package/build-module/form/primitives/fieldset/legend.mjs.map +2 -2
  93. package/build-module/index.mjs +6 -0
  94. package/build-module/index.mjs.map +2 -2
  95. package/build-module/link/index.mjs +6 -0
  96. package/build-module/link/index.mjs.map +7 -0
  97. package/build-module/link/link.mjs +90 -0
  98. package/build-module/link/link.mjs.map +7 -0
  99. package/build-module/link/types.mjs +1 -0
  100. package/build-module/link/types.mjs.map +7 -0
  101. package/build-module/notice/action-button.mjs +3 -3
  102. package/build-module/notice/action-button.mjs.map +2 -2
  103. package/build-module/notice/action-link.mjs +17 -18
  104. package/build-module/notice/action-link.mjs.map +2 -2
  105. package/build-module/notice/actions.mjs +3 -3
  106. package/build-module/notice/actions.mjs.map +2 -2
  107. package/build-module/notice/close-icon.mjs +3 -3
  108. package/build-module/notice/close-icon.mjs.map +2 -2
  109. package/build-module/notice/description.mjs +16 -15
  110. package/build-module/notice/description.mjs.map +2 -2
  111. package/build-module/notice/root.mjs +3 -3
  112. package/build-module/notice/root.mjs.map +2 -2
  113. package/build-module/notice/title.mjs +16 -12
  114. package/build-module/notice/title.mjs.map +2 -2
  115. package/build-module/text/index.mjs +6 -0
  116. package/build-module/text/index.mjs.map +7 -0
  117. package/build-module/text/text.mjs +30 -0
  118. package/build-module/text/text.mjs.map +7 -0
  119. package/build-module/text/types.mjs +1 -0
  120. package/build-module/text/types.mjs.map +7 -0
  121. package/build-module/tooltip/popup.mjs +1 -1
  122. package/build-module/tooltip/popup.mjs.map +1 -1
  123. package/build-module/visually-hidden/visually-hidden.mjs +3 -3
  124. package/build-module/visually-hidden/visually-hidden.mjs.map +2 -2
  125. package/build-types/card/content.d.ts +6 -0
  126. package/build-types/card/content.d.ts.map +1 -0
  127. package/build-types/card/full-bleed.d.ts +9 -0
  128. package/build-types/card/full-bleed.d.ts.map +1 -0
  129. package/build-types/card/header.d.ts +7 -0
  130. package/build-types/card/header.d.ts.map +1 -0
  131. package/build-types/card/index.d.ts +7 -0
  132. package/build-types/card/index.d.ts.map +1 -0
  133. package/build-types/card/root.d.ts +23 -0
  134. package/build-types/card/root.d.ts.map +1 -0
  135. package/build-types/card/stories/index.story.d.ts +22 -0
  136. package/build-types/card/stories/index.story.d.ts.map +1 -0
  137. package/build-types/card/test/index.test.d.ts +2 -0
  138. package/build-types/card/test/index.test.d.ts.map +1 -0
  139. package/build-types/card/title.d.ts +7 -0
  140. package/build-types/card/title.d.ts.map +1 -0
  141. package/build-types/card/types.d.ts +34 -0
  142. package/build-types/card/types.d.ts.map +1 -0
  143. package/build-types/collapsible-card/content.d.ts +7 -0
  144. package/build-types/collapsible-card/content.d.ts.map +1 -0
  145. package/build-types/collapsible-card/header.d.ts +12 -0
  146. package/build-types/collapsible-card/header.d.ts.map +1 -0
  147. package/build-types/collapsible-card/index.d.ts +5 -0
  148. package/build-types/collapsible-card/index.d.ts.map +1 -0
  149. package/build-types/collapsible-card/root.d.ts +24 -0
  150. package/build-types/collapsible-card/root.d.ts.map +1 -0
  151. package/build-types/collapsible-card/stories/index.story.d.ts +23 -0
  152. package/build-types/collapsible-card/stories/index.story.d.ts.map +1 -0
  153. package/build-types/collapsible-card/test/index.test.d.ts +2 -0
  154. package/build-types/collapsible-card/test/index.test.d.ts.map +1 -0
  155. package/build-types/collapsible-card/types.d.ts +42 -0
  156. package/build-types/collapsible-card/types.d.ts.map +1 -0
  157. package/build-types/form/primitives/field/label.d.ts +1 -0
  158. package/build-types/form/primitives/field/label.d.ts.map +1 -1
  159. package/build-types/form/primitives/field/stories/index.story.d.ts +5 -0
  160. package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
  161. package/build-types/form/primitives/field/types.d.ts +7 -0
  162. package/build-types/form/primitives/field/types.d.ts.map +1 -1
  163. package/build-types/form/primitives/fieldset/legend.d.ts +1 -0
  164. package/build-types/form/primitives/fieldset/legend.d.ts.map +1 -1
  165. package/build-types/form/primitives/fieldset/stories/index.story.d.ts +5 -0
  166. package/build-types/form/primitives/fieldset/stories/index.story.d.ts.map +1 -1
  167. package/build-types/form/primitives/fieldset/types.d.ts +7 -0
  168. package/build-types/form/primitives/fieldset/types.d.ts.map +1 -1
  169. package/build-types/index.d.ts +4 -0
  170. package/build-types/index.d.ts.map +1 -1
  171. package/build-types/link/index.d.ts +2 -0
  172. package/build-types/link/index.d.ts.map +1 -0
  173. package/build-types/link/link.d.ts +7 -0
  174. package/build-types/link/link.d.ts.map +1 -0
  175. package/build-types/link/stories/index.story.d.ts +18 -0
  176. package/build-types/link/stories/index.story.d.ts.map +1 -0
  177. package/build-types/link/test/index.test.d.ts +2 -0
  178. package/build-types/link/test/index.test.d.ts.map +1 -0
  179. package/build-types/link/types.d.ts +33 -0
  180. package/build-types/link/types.d.ts.map +1 -0
  181. package/build-types/notice/action-link.d.ts +0 -2
  182. package/build-types/notice/action-link.d.ts.map +1 -1
  183. package/build-types/notice/description.d.ts +1 -1
  184. package/build-types/notice/description.d.ts.map +1 -1
  185. package/build-types/notice/title.d.ts.map +1 -1
  186. package/build-types/notice/types.d.ts +3 -2
  187. package/build-types/notice/types.d.ts.map +1 -1
  188. package/build-types/text/index.d.ts +2 -0
  189. package/build-types/text/index.d.ts.map +1 -0
  190. package/build-types/text/stories/index.story.d.ts +9 -0
  191. package/build-types/text/stories/index.story.d.ts.map +1 -0
  192. package/build-types/text/test/index.test.d.ts +2 -0
  193. package/build-types/text/test/index.test.d.ts.map +1 -0
  194. package/build-types/text/text.d.ts +7 -0
  195. package/build-types/text/text.d.ts.map +1 -0
  196. package/build-types/text/types.d.ts +15 -0
  197. package/build-types/text/types.d.ts.map +1 -0
  198. package/package.json +11 -11
  199. package/src/card/content.tsx +20 -0
  200. package/src/card/full-bleed.tsx +26 -0
  201. package/src/card/header.tsx +21 -0
  202. package/src/card/index.ts +7 -0
  203. package/src/card/root.tsx +42 -0
  204. package/src/card/stories/index.story.tsx +128 -0
  205. package/src/card/style.module.css +47 -0
  206. package/src/card/test/index.test.tsx +96 -0
  207. package/src/card/title.tsx +22 -0
  208. package/src/card/types.ts +38 -0
  209. package/src/collapsible-card/content.tsx +20 -0
  210. package/src/collapsible-card/header.tsx +71 -0
  211. package/src/collapsible-card/index.ts +5 -0
  212. package/src/collapsible-card/root.tsx +37 -0
  213. package/src/collapsible-card/stories/index.story.tsx +156 -0
  214. package/src/collapsible-card/style.module.css +37 -0
  215. package/src/collapsible-card/test/index.test.tsx +207 -0
  216. package/src/collapsible-card/types.ts +44 -0
  217. package/src/form/primitives/field/label.tsx +9 -1
  218. package/src/form/primitives/field/stories/index.story.tsx +17 -0
  219. package/src/form/primitives/field/test/index.test.tsx +13 -0
  220. package/src/form/primitives/field/types.ts +7 -0
  221. package/src/form/primitives/fieldset/legend.tsx +8 -1
  222. package/src/form/primitives/fieldset/stories/index.story.tsx +20 -0
  223. package/src/form/primitives/fieldset/test/index.test.tsx +14 -0
  224. package/src/form/primitives/fieldset/types.ts +7 -0
  225. package/src/index.ts +4 -0
  226. package/src/link/index.ts +1 -0
  227. package/src/link/link.tsx +73 -0
  228. package/src/link/stories/index.story.tsx +92 -0
  229. package/src/link/style.module.css +68 -0
  230. package/src/link/test/index.test.tsx +93 -0
  231. package/src/link/types.ts +36 -0
  232. package/src/notice/action-link.tsx +12 -18
  233. package/src/notice/description.tsx +12 -14
  234. package/src/notice/style.module.css +9 -22
  235. package/src/notice/test/index.test.tsx +2 -2
  236. package/src/notice/title.tsx +11 -10
  237. package/src/notice/types.ts +3 -2
  238. package/src/text/index.ts +1 -0
  239. package/src/text/stories/index.story.tsx +68 -0
  240. package/src/text/style.module.css +67 -0
  241. package/src/text/test/index.test.tsx +46 -0
  242. package/src/text/text.tsx +25 -0
  243. package/src/text/types.ts +25 -0
  244. package/src/tooltip/popup.tsx +1 -1
  245. package/src/visually-hidden/style.module.css +1 -0
@@ -0,0 +1,21 @@
1
+ import { mergeProps, useRender } from '@base-ui/react';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import styles from './style.module.css';
4
+ import type { HeaderProps } from './types';
5
+
6
+ /**
7
+ * A structural container for the card's heading area, typically containing
8
+ * `Card.Title`.
9
+ */
10
+ export const Header = forwardRef< HTMLDivElement, HeaderProps >(
11
+ function CardHeader( { render, ...props }, ref ) {
12
+ const element = useRender( {
13
+ defaultTagName: 'div',
14
+ render,
15
+ ref,
16
+ props: mergeProps< 'div' >( { className: styles.header }, props ),
17
+ } );
18
+
19
+ return element;
20
+ }
21
+ );
@@ -0,0 +1,7 @@
1
+ import { Root } from './root';
2
+ import { Header } from './header';
3
+ import { Content } from './content';
4
+ import { FullBleed } from './full-bleed';
5
+ import { Title } from './title';
6
+
7
+ export { Root, Header, Content, FullBleed, Title };
@@ -0,0 +1,42 @@
1
+ import { mergeProps, useRender } from '@base-ui/react';
2
+ import clsx from 'clsx';
3
+ import { forwardRef } from '@wordpress/element';
4
+ import resetStyles from '../utils/css/resets.module.css';
5
+ import styles from './style.module.css';
6
+ import type { RootProps } from './types';
7
+
8
+ /**
9
+ * A visually contained surface that groups related content and actions.
10
+ *
11
+ * ```jsx
12
+ * import { Card } from '@wordpress/ui';
13
+ *
14
+ * function MyComponent() {
15
+ * return (
16
+ * <Card.Root>
17
+ * <Card.Header>
18
+ * <Card.Title>Heading</Card.Title>
19
+ * </Card.Header>
20
+ * <Card.Content>
21
+ * <p>Main content here.</p>
22
+ * </Card.Content>
23
+ * </Card.Root>
24
+ * );
25
+ * }
26
+ * ```
27
+ */
28
+ export const Root = forwardRef< HTMLDivElement, RootProps >( function Card(
29
+ { render, ...restProps },
30
+ ref
31
+ ) {
32
+ const mergedClassName = clsx( styles.root, resetStyles[ 'box-sizing' ] );
33
+
34
+ const element = useRender( {
35
+ defaultTagName: 'div',
36
+ render,
37
+ ref,
38
+ props: mergeProps< 'div' >( { className: mergedClassName }, restProps ),
39
+ } );
40
+
41
+ return element;
42
+ } );
@@ -0,0 +1,128 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import * as Card from '../index';
3
+
4
+ /**
5
+ * Temporary text component for story examples. This will be replaced by an
6
+ * official DS `<Text />` component once it's available.
7
+ */
8
+ function Text( { children }: { children: React.ReactNode } ) {
9
+ return (
10
+ <p
11
+ style={ {
12
+ margin: 0,
13
+ fontFamily: [ 'var(--wp', 'ds-font-family-body)' ].join( '' ),
14
+ fontSize: 'var(--wpds-font-size-md)',
15
+ fontWeight: 'var(--wpds-font-weight-regular)',
16
+ lineHeight: 'var(--wpds-font-line-height-sm)',
17
+ textWrap: 'pretty',
18
+ color: 'var(--wpds-color-fg-content-neutral-weak)',
19
+ } }
20
+ >
21
+ { children }
22
+ </p>
23
+ );
24
+ }
25
+
26
+ const meta: Meta< typeof Card.Root > = {
27
+ title: 'Design System/Components/Card',
28
+ component: Card.Root,
29
+ subcomponents: {
30
+ 'Card.Header': Card.Header,
31
+ 'Card.Content': Card.Content,
32
+ 'Card.FullBleed': Card.FullBleed,
33
+ 'Card.Title': Card.Title,
34
+ },
35
+ };
36
+ export default meta;
37
+
38
+ type Story = StoryObj< typeof Card.Root >;
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ children: (
43
+ <>
44
+ <Card.Header>
45
+ <Card.Title>Card title</Card.Title>
46
+ </Card.Header>
47
+ <Card.Content>
48
+ <Text>
49
+ This is the main content area. It can contain any
50
+ elements. This is the main content area. It can contain
51
+ any elements. This is the main content area. It can
52
+ contain any elements. This is the main content area. It
53
+ can contain any elements. This is the main content area.
54
+ It can contain any elements. This is the main content
55
+ area. It can contain any elements.
56
+ </Text>
57
+ <Text>
58
+ This is the main content area. It can contain any
59
+ elements.
60
+ </Text>
61
+ </Card.Content>
62
+ </>
63
+ ),
64
+ },
65
+ };
66
+
67
+ /**
68
+ * `Card.FullBleed` breaks out of the card's padding to span
69
+ * edge-to-edge. Useful for images, dividers, or embedded content.
70
+ */
71
+ export const WithFullBleed: Story = {
72
+ args: {
73
+ children: (
74
+ <>
75
+ <Card.Header>
76
+ <Card.Title>Featured image</Card.Title>
77
+ </Card.Header>
78
+ <Card.Content>
79
+ <Card.FullBleed>
80
+ <div
81
+ style={ {
82
+ height: 160,
83
+ background:
84
+ 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
85
+ } }
86
+ />
87
+ </Card.FullBleed>
88
+ <Text>Content below the full-bleed area.</Text>
89
+ </Card.Content>
90
+ </>
91
+ ),
92
+ },
93
+ };
94
+
95
+ /**
96
+ * A minimal card with only a header.
97
+ */
98
+ export const HeaderOnly: Story = {
99
+ args: {
100
+ children: (
101
+ <Card.Header>
102
+ <Card.Title>Simple card</Card.Title>
103
+ </Card.Header>
104
+ ),
105
+ },
106
+ };
107
+
108
+ /**
109
+ * Use the `render` prop to change the underlying HTML elements for
110
+ * better semantics. Here, `Card.Root` renders as a `<section>` and
111
+ * `Card.Title` renders as an `<h2>`.
112
+ */
113
+ export const CustomSemantics: Story = {
114
+ args: {
115
+ render: <section />,
116
+ children: (
117
+ <>
118
+ <Card.Header>
119
+ { /* eslint-disable-next-line jsx-a11y/heading-has-content -- content provided via render prop */ }
120
+ <Card.Title render={ <h2 /> }>Section heading</Card.Title>
121
+ </Card.Header>
122
+ <Card.Content>
123
+ <Text>Semantically meaningful card content.</Text>
124
+ </Card.Content>
125
+ </>
126
+ ),
127
+ },
128
+ };
@@ -0,0 +1,47 @@
1
+ @layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;
2
+
3
+ @layer wp-ui-components {
4
+ .root {
5
+ --wp-ui-card-padding: var(--wpds-dimension-padding-xl);
6
+ --wp-ui-card-header-content-gap: var(--wpds-dimension-gap-lg);
7
+
8
+ display: flex;
9
+ flex-direction: column;
10
+ border: 1px solid var(--wpds-color-stroke-surface-neutral-weak);
11
+ border-radius: var(--wpds-border-radius-lg);
12
+ background-color: var(--wpds-color-bg-surface-neutral-strong);
13
+ }
14
+
15
+ /* Padding is applied to the individual header/content elements to enable
16
+ * the higher-level `CollapsibleCard` component to be fully clickable
17
+ * to expand/collapse the card.
18
+ */
19
+ .header,
20
+ .content {
21
+ padding: var(--wp-ui-card-padding);
22
+
23
+ &:not(:first-child):not(:last-child) {
24
+ padding-block-end: 0;
25
+ }
26
+ }
27
+
28
+ /* Custom vertical gap between header and content */
29
+ .header + .content {
30
+ padding-block-start: 0;
31
+ margin-block-start: calc(var(--wp-ui-card-header-content-gap) - var(--wp-ui-card-padding));
32
+ }
33
+
34
+ .fullbleed {
35
+ margin-inline: calc(-1 * var(--wp-ui-card-padding));
36
+ width: calc(100% + 2 * var(--wp-ui-card-padding));
37
+ }
38
+
39
+ .title {
40
+ margin: 0;
41
+ font-family: var(--wpds-font-family-heading);
42
+ font-size: var(--wpds-font-size-lg);
43
+ font-weight: var(--wpds-font-weight-medium);
44
+ line-height: var(--wpds-font-line-height-sm);
45
+ color: var(--wpds-color-fg-content-neutral);
46
+ }
47
+ }
@@ -0,0 +1,96 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { createRef } from '@wordpress/element';
3
+ import * as Card from '../index';
4
+
5
+ describe( 'Card', () => {
6
+ describe( 'basic behaviour', () => {
7
+ it( 'forwards ref', () => {
8
+ const rootRef = createRef< HTMLDivElement >();
9
+ const headerRef = createRef< HTMLDivElement >();
10
+ const contentRef = createRef< HTMLDivElement >();
11
+ const fullBleedRef = createRef< HTMLDivElement >();
12
+ const titleRef = createRef< HTMLDivElement >();
13
+
14
+ render(
15
+ <Card.Root ref={ rootRef }>
16
+ <Card.Header ref={ headerRef }>
17
+ <Card.Title ref={ titleRef }>Title</Card.Title>
18
+ </Card.Header>
19
+ <Card.Content ref={ contentRef }>
20
+ <Card.FullBleed ref={ fullBleedRef }>
21
+ Full width
22
+ </Card.FullBleed>
23
+ </Card.Content>
24
+ </Card.Root>
25
+ );
26
+
27
+ expect( rootRef.current ).toBeInstanceOf( HTMLDivElement );
28
+ expect( headerRef.current ).toBeInstanceOf( HTMLDivElement );
29
+ expect( contentRef.current ).toBeInstanceOf( HTMLDivElement );
30
+ expect( fullBleedRef.current ).toBeInstanceOf( HTMLDivElement );
31
+ expect( titleRef.current ).toBeInstanceOf( HTMLDivElement );
32
+ } );
33
+
34
+ it( 'renders content', () => {
35
+ render(
36
+ <Card.Root>
37
+ <Card.Header>
38
+ <Card.Title>Card heading</Card.Title>
39
+ </Card.Header>
40
+ <Card.Content>
41
+ <p>Main content</p>
42
+ </Card.Content>
43
+ </Card.Root>
44
+ );
45
+
46
+ expect( screen.getByText( 'Card heading' ) ).toBeVisible();
47
+ expect( screen.getByText( 'Main content' ) ).toBeVisible();
48
+ } );
49
+ } );
50
+
51
+ describe( 'fullbleed', () => {
52
+ it( 'renders children', () => {
53
+ render(
54
+ <Card.Root>
55
+ <Card.Content>
56
+ <Card.FullBleed>
57
+ <img
58
+ src="https://example.com/image.jpg"
59
+ alt="test"
60
+ />
61
+ </Card.FullBleed>
62
+ </Card.Content>
63
+ </Card.Root>
64
+ );
65
+
66
+ expect( screen.getByRole( 'img', { name: 'test' } ) ).toBeVisible();
67
+ } );
68
+ } );
69
+
70
+ describe( 'render prop', () => {
71
+ it( 'allows Root to render as a different element', () => {
72
+ render(
73
+ <Card.Root render={ <section /> } data-testid="card">
74
+ <Card.Content>Content</Card.Content>
75
+ </Card.Root>
76
+ );
77
+
78
+ expect( screen.getByTestId( 'card' ).tagName ).toBe( 'SECTION' );
79
+ } );
80
+
81
+ it( 'allows Title to render as a heading element', () => {
82
+ render(
83
+ <Card.Root>
84
+ <Card.Header>
85
+ { /* eslint-disable-next-line jsx-a11y/heading-has-content -- content provided via render prop */ }
86
+ <Card.Title render={ <h2 /> }>Heading</Card.Title>
87
+ </Card.Header>
88
+ </Card.Root>
89
+ );
90
+
91
+ expect(
92
+ screen.getByRole( 'heading', { level: 2, name: 'Heading' } )
93
+ ).toBeVisible();
94
+ } );
95
+ } );
96
+ } );
@@ -0,0 +1,22 @@
1
+ import { mergeProps, useRender } from '@base-ui/react';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import styles from './style.module.css';
4
+ import type { TitleProps } from './types';
5
+
6
+ /**
7
+ * The title for a card. Renders as a `<div>` by default — use the `render`
8
+ * prop to swap in a semantic heading element when appropriate.
9
+ */
10
+ export const Title = forwardRef< HTMLDivElement, TitleProps >(
11
+ function CardTitle( { render, ...props }, ref ) {
12
+ const element = useRender( {
13
+ defaultTagName: 'div',
14
+ render,
15
+ ref,
16
+ // TODO: use `Text` component instead, when ready
17
+ props: mergeProps< 'div' >( { className: styles.title }, props ),
18
+ } );
19
+
20
+ return element;
21
+ }
22
+ );
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ComponentProps } from '../utils/types';
3
+
4
+ export interface RootProps extends ComponentProps< 'div' > {
5
+ /**
6
+ * The content to be rendered inside the card.
7
+ */
8
+ children?: ReactNode;
9
+ }
10
+
11
+ export interface HeaderProps extends ComponentProps< 'div' > {
12
+ /**
13
+ * The content to be rendered inside the header.
14
+ */
15
+ children?: ReactNode;
16
+ }
17
+
18
+ export interface ContentProps extends ComponentProps< 'div' > {
19
+ /**
20
+ * The content to be rendered inside the content area.
21
+ */
22
+ children?: ReactNode;
23
+ }
24
+
25
+ export interface FullBleedProps extends ComponentProps< 'div' > {
26
+ /**
27
+ * The content to be rendered edge-to-edge, breaking out of the
28
+ * card's padding.
29
+ */
30
+ children?: ReactNode;
31
+ }
32
+
33
+ export interface TitleProps extends ComponentProps< 'div' > {
34
+ /**
35
+ * The title text for the card.
36
+ */
37
+ children?: ReactNode;
38
+ }
@@ -0,0 +1,20 @@
1
+ import { Collapsible } from '@base-ui/react/collapsible';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import * as Card from '../card';
4
+ import type { ContentProps } from './types';
5
+
6
+ /**
7
+ * The collapsible content area of the card. Hidden when collapsed,
8
+ * visible when expanded.
9
+ */
10
+ export const Content = forwardRef< HTMLDivElement, ContentProps >(
11
+ function CollapsibleCardContent( { render, ...restProps }, ref ) {
12
+ return (
13
+ <Collapsible.Panel
14
+ ref={ ref }
15
+ render={ <Card.Content render={ render } /> }
16
+ { ...restProps }
17
+ />
18
+ );
19
+ }
20
+ );
@@ -0,0 +1,71 @@
1
+ import { Collapsible } from '@base-ui/react/collapsible';
2
+ import clsx from 'clsx';
3
+ import type { MouseEvent } from 'react';
4
+ import { forwardRef, useCallback, useRef } from '@wordpress/element';
5
+ import { __ } from '@wordpress/i18n';
6
+ import { chevronDown, chevronUp } from '@wordpress/icons';
7
+ import * as Card from '../card';
8
+ import { IconButton } from '../icon-button';
9
+ import styles from './style.module.css';
10
+ import type { HeaderProps } from './types';
11
+
12
+ /**
13
+ * The header of a collapsible card. Always visible, and acts as the
14
+ * toggle trigger — clicking anywhere on it expands or collapses the
15
+ * card's content.
16
+ *
17
+ * Avoid placing interactive elements (buttons, links, inputs) inside the
18
+ * header, since the entire area is clickable and their events will bubble
19
+ * to trigger the collapse toggle.
20
+ */
21
+ export const Header = forwardRef< HTMLDivElement, HeaderProps >(
22
+ function CollapsibleCardHeader(
23
+ { children, className, onClick, ...restProps },
24
+ ref
25
+ ) {
26
+ const triggerRef = useRef< HTMLButtonElement >( null );
27
+
28
+ const handleHeaderClick = useCallback(
29
+ ( event: MouseEvent< HTMLDivElement > ) => {
30
+ const trigger = triggerRef.current;
31
+ if (
32
+ trigger &&
33
+ event.target instanceof Node &&
34
+ ! trigger.contains( event.target )
35
+ ) {
36
+ trigger.click();
37
+ }
38
+
39
+ onClick?.( event );
40
+ },
41
+ [ onClick ]
42
+ );
43
+
44
+ return (
45
+ <Card.Header
46
+ ref={ ref }
47
+ className={ clsx( styles.header, className ) }
48
+ onClick={ handleHeaderClick }
49
+ { ...restProps }
50
+ >
51
+ <div className={ styles[ 'header-content' ] }>{ children }</div>
52
+ <div className={ styles[ 'header-trigger-wrapper' ] }>
53
+ <Collapsible.Trigger
54
+ ref={ triggerRef }
55
+ render={ ( props, state ) => (
56
+ <IconButton
57
+ { ...props }
58
+ label={ __( 'Expand or collapse card' ) }
59
+ icon={ state.open ? chevronUp : chevronDown }
60
+ variant="minimal"
61
+ tone="neutral"
62
+ size="compact"
63
+ />
64
+ ) }
65
+ className={ styles[ 'header-trigger' ] }
66
+ />
67
+ </div>
68
+ </Card.Header>
69
+ );
70
+ }
71
+ );
@@ -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 { Collapsible } from '@base-ui/react/collapsible';
2
+ import { forwardRef } from '@wordpress/element';
3
+ import * as Card from '../card';
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,156 @@
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(--wp', 'ds-font-family-body)' ].join( '' ),
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
+ * Visual comparison: a `CollapsibleCard` (open) next to a regular `Card`
110
+ * to verify identical spacing and layout.
111
+ */
112
+ export const ComparedToCard: Story = {
113
+ // `defaultOpen` (uncontrolled) and `open` (controlled) should not be
114
+ // used together — disable the `open` control to avoid confusion.
115
+ argTypes: { open: { control: false } },
116
+ args: {
117
+ ...Default.args,
118
+ defaultOpen: true,
119
+ },
120
+ render: ( { open, defaultOpen, onOpenChange, disabled, ...restArgs } ) => (
121
+ <div
122
+ style={ {
123
+ display: 'flex',
124
+ flexDirection: 'column',
125
+ gap: 'var( --wpds-dimension-gap-lg )',
126
+ } }
127
+ >
128
+ <CollapsibleCard.Root
129
+ open={ open }
130
+ defaultOpen={ defaultOpen }
131
+ onOpenChange={ onOpenChange }
132
+ disabled={ disabled }
133
+ { ...restArgs }
134
+ >
135
+ <CollapsibleCard.Header>
136
+ <Card.Title>CollapsibleCard (open)</Card.Title>
137
+ </CollapsibleCard.Header>
138
+ <CollapsibleCard.Content>
139
+ <Text>
140
+ Content should align with the regular card below.
141
+ </Text>
142
+ </CollapsibleCard.Content>
143
+ </CollapsibleCard.Root>
144
+ <Card.Root { ...restArgs }>
145
+ <Card.Header>
146
+ <Card.Title>Regular Card</Card.Title>
147
+ </Card.Header>
148
+ <Card.Content>
149
+ <Text>
150
+ Content should align with the collapsible card above.
151
+ </Text>
152
+ </Card.Content>
153
+ </Card.Root>
154
+ </div>
155
+ ),
156
+ };