newspack-components 4.6.2 → 4.7.0-alpha.1
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.
- package/CHANGELOG.md +23 -0
- package/colors/colors.module.scss +13 -7
- package/dist/cjs/autocomplete-tokenfield/style.scss +4 -2
- package/dist/cjs/breadcrumbs/format-count.js +33 -0
- package/dist/cjs/breadcrumbs/index.js +78 -20
- package/dist/cjs/breadcrumbs/index.test.js +110 -0
- package/dist/cjs/breadcrumbs/style.scss +6 -0
- package/dist/cjs/button/index.js +41 -17
- package/dist/cjs/button/index.test.js +273 -0
- package/dist/cjs/card/core-card.js +9 -4
- package/dist/cjs/card/core-card.test.js +29 -0
- package/dist/cjs/card/index.js +4 -2
- package/dist/cjs/card/style-core.scss +59 -0
- package/dist/cjs/card-feature/index.js +7 -1
- package/dist/cjs/card-feature/index.test.js +92 -0
- package/dist/cjs/color-picker/index.js +1 -1
- package/dist/cjs/confirm-dialog/index.js +26 -9
- package/dist/cjs/confirm-dialog/index.test.js +176 -0
- package/dist/cjs/dataviews/index.js +11 -2
- package/dist/cjs/divider/index.js +13 -0
- package/dist/cjs/drawer/action.js +74 -0
- package/dist/cjs/drawer/body.js +79 -0
- package/dist/cjs/drawer/close-icon.js +45 -0
- package/dist/cjs/drawer/content.js +72 -0
- package/dist/cjs/drawer/context.js +23 -0
- package/dist/cjs/drawer/context.test.js +43 -0
- package/dist/cjs/drawer/divider.js +24 -0
- package/dist/cjs/drawer/footer.js +26 -0
- package/dist/cjs/drawer/header.js +26 -0
- package/dist/cjs/drawer/index.js +42 -0
- package/dist/cjs/drawer/index.test.js +1287 -0
- package/dist/cjs/drawer/root.js +350 -0
- package/dist/cjs/drawer/slots.test.js +422 -0
- package/dist/cjs/drawer/style.scss +205 -0
- package/dist/cjs/drawer/title.js +54 -0
- package/dist/cjs/drawer/types.js +5 -0
- package/dist/cjs/drawer/use-exit-animation.js +59 -0
- package/dist/cjs/hooks/use-confirm-dialog.js +15 -3
- package/dist/cjs/hooks/use-unsaved-changes-dialog.js +19 -4
- package/dist/cjs/index.js +28 -1
- package/dist/cjs/page/index.js +1 -1
- package/dist/cjs/page-control/index.js +82 -0
- package/dist/cjs/plugin-settings/SettingsSection.js +7 -1
- package/dist/cjs/section-header/index.js +5 -2
- package/dist/cjs/section-header/style.scss +25 -12
- package/dist/cjs/table-card/index.js +67 -0
- package/dist/cjs/table-card/index.test.js +153 -0
- package/dist/cjs/with-wizard-screen/style.scss +8 -0
- package/dist/cjs/wizard/index.js +102 -38
- package/dist/esm/autocomplete-tokenfield/style.scss +4 -2
- package/dist/esm/breadcrumbs/format-count.js +27 -0
- package/dist/esm/breadcrumbs/index.js +81 -20
- package/dist/esm/breadcrumbs/index.test.js +110 -0
- package/dist/esm/breadcrumbs/style.scss +6 -0
- package/dist/esm/button/index.js +42 -18
- package/dist/esm/button/index.test.js +273 -0
- package/dist/esm/card/core-card.js +9 -4
- package/dist/esm/card/core-card.test.js +29 -0
- package/dist/esm/card/index.js +4 -2
- package/dist/esm/card/style-core.scss +59 -0
- package/dist/esm/card-feature/index.js +8 -2
- package/dist/esm/card-feature/index.test.js +96 -0
- package/dist/esm/color-picker/index.js +1 -1
- package/dist/esm/confirm-dialog/index.js +26 -9
- package/dist/esm/confirm-dialog/index.test.js +176 -0
- package/dist/esm/dataviews/index.js +11 -2
- package/dist/esm/divider/index.js +14 -0
- package/dist/esm/drawer/action.js +68 -0
- package/dist/esm/drawer/body.js +70 -0
- package/dist/esm/drawer/close-icon.js +37 -0
- package/dist/esm/drawer/content.js +63 -0
- package/dist/esm/drawer/context.js +17 -0
- package/dist/esm/drawer/context.test.js +40 -0
- package/dist/esm/drawer/divider.js +16 -0
- package/dist/esm/drawer/footer.js +18 -0
- package/dist/esm/drawer/header.js +18 -0
- package/dist/esm/drawer/index.js +35 -0
- package/dist/esm/drawer/index.test.js +1285 -0
- package/dist/esm/drawer/root.js +342 -0
- package/dist/esm/drawer/slots.test.js +422 -0
- package/dist/esm/drawer/style.scss +205 -0
- package/dist/esm/drawer/title.js +46 -0
- package/dist/esm/drawer/types.js +1 -0
- package/dist/esm/drawer/use-exit-animation.js +52 -0
- package/dist/esm/hooks/use-confirm-dialog.js +15 -3
- package/dist/esm/hooks/use-unsaved-changes-dialog.js +20 -5
- package/dist/esm/index.js +4 -0
- package/dist/esm/page/index.js +1 -1
- package/dist/esm/page-control/index.js +82 -0
- package/dist/esm/plugin-settings/SettingsSection.js +8 -2
- package/dist/esm/section-header/index.js +5 -2
- package/dist/esm/section-header/style.scss +25 -12
- package/dist/esm/table-card/index.js +60 -0
- package/dist/esm/table-card/index.test.js +150 -0
- package/dist/esm/with-wizard-screen/style.scss +8 -0
- package/dist/esm/wizard/index.js +104 -38
- package/package.json +2 -2
- package/src/autocomplete-tokenfield/style.scss +4 -2
- package/src/breadcrumbs/format-count.js +27 -0
- package/src/breadcrumbs/index.js +68 -8
- package/src/breadcrumbs/index.test.js +57 -0
- package/src/breadcrumbs/style.scss +6 -0
- package/src/button/index.test.js +143 -0
- package/src/button/index.tsx +32 -7
- package/src/card/core-card.js +15 -2
- package/src/card/core-card.test.js +19 -0
- package/src/card/index.js +3 -0
- package/src/card/style-core.scss +59 -0
- package/src/card-feature/README.md +4 -0
- package/src/card-feature/index.test.js +65 -0
- package/src/card-feature/index.tsx +13 -2
- package/src/color-picker/index.js +1 -1
- package/src/confirm-dialog/README.md +3 -2
- package/src/confirm-dialog/index.test.js +146 -0
- package/src/confirm-dialog/index.tsx +23 -9
- package/src/dataviews/index.tsx +11 -2
- package/src/divider/index.js +13 -0
- package/src/drawer/README.md +454 -0
- package/src/drawer/action.tsx +67 -0
- package/src/drawer/body.tsx +60 -0
- package/src/drawer/close-icon.tsx +37 -0
- package/src/drawer/content.tsx +54 -0
- package/src/drawer/context.test.js +31 -0
- package/src/drawer/context.ts +25 -0
- package/src/drawer/divider.tsx +13 -0
- package/src/drawer/footer.tsx +15 -0
- package/src/drawer/header.tsx +15 -0
- package/src/drawer/index.test.js +1012 -0
- package/src/drawer/index.tsx +33 -0
- package/src/drawer/root.tsx +328 -0
- package/src/drawer/slots.test.js +314 -0
- package/src/drawer/style.scss +205 -0
- package/src/drawer/title.tsx +40 -0
- package/src/drawer/types.ts +79 -0
- package/src/drawer/use-exit-animation.ts +46 -0
- package/src/hooks/use-confirm-dialog.tsx +14 -3
- package/src/hooks/use-unsaved-changes-dialog.tsx +17 -5
- package/src/index.js +4 -0
- package/src/page/index.js +1 -1
- package/src/page-control/index.js +63 -0
- package/src/plugin-settings/SettingsSection.js +8 -2
- package/src/section-header/index.js +5 -2
- package/src/section-header/style.scss +25 -12
- package/src/table-card/README.md +100 -0
- package/src/table-card/index.test.js +112 -0
- package/src/table-card/index.tsx +56 -0
- package/src/with-wizard-screen/style.scss +8 -0
- package/src/wizard/index.js +95 -34
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# Drawer
|
|
2
|
+
|
|
3
|
+
A modal panel that slides in from the right edge and spans the full height of
|
|
4
|
+
the viewport. The page behind it is inert.
|
|
5
|
+
|
|
6
|
+
The API is compound: a `Drawer.Root` and one subcomponent per slot. It mirrors
|
|
7
|
+
the `Drawer` that ships in `@wordpress/ui`, so adopting that package later, once
|
|
8
|
+
it stabilises, is close to a rename inside this wrapper. `Drawer.Divider` is
|
|
9
|
+
the one part with no counterpart there.
|
|
10
|
+
|
|
11
|
+
The parts hang off one exported object rather than the flat named exports the
|
|
12
|
+
rest of this package uses. That is deliberate, to keep them recognisable against
|
|
13
|
+
`@wordpress/ui`'s own `Drawer`, and not a pattern for the next compound
|
|
14
|
+
component to copy.
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
import { Drawer } from 'newspack-components';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The component imports its own stylesheet, so the barrel ships the CSS with it.
|
|
21
|
+
There is nothing separate to import.
|
|
22
|
+
|
|
23
|
+
## Keep it mounted
|
|
24
|
+
|
|
25
|
+
**`Drawer.Root` takes `isOpen` and stays mounted.** It owns its own exit
|
|
26
|
+
animation, so it has to still be in the tree while the slide-out plays.
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
// Yes.
|
|
30
|
+
<Drawer.Root isOpen={ isOpen } onRequestClose={ close }>
|
|
31
|
+
{ slots }
|
|
32
|
+
</Drawer.Root>
|
|
33
|
+
|
|
34
|
+
// No. Unmounting cuts the slide-out, and the panel vanishes instead.
|
|
35
|
+
{ isOpen && <Drawer.Root isOpen onRequestClose={ close }>{ slots }</Drawer.Root> }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It renders nothing while closed, so leaving it mounted costs nothing.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```jsx
|
|
43
|
+
import { __ } from '@wordpress/i18n';
|
|
44
|
+
import { useState } from '@wordpress/element';
|
|
45
|
+
import { Icon, settings } from '@wordpress/icons';
|
|
46
|
+
import { Drawer } from 'newspack-components';
|
|
47
|
+
|
|
48
|
+
const [ isOpen, setIsOpen ] = useState( false );
|
|
49
|
+
|
|
50
|
+
<Drawer.Root isOpen={ isOpen } size="large" isDirty={ isDirty } onRequestClose={ () => setIsOpen( false ) }>
|
|
51
|
+
<Drawer.Header>
|
|
52
|
+
<Icon className="newspack-drawer__icon" icon={ settings } size={ 24 } />
|
|
53
|
+
<Drawer.Title>{ __( 'Edit Styles', 'newspack-plugin' ) }</Drawer.Title>
|
|
54
|
+
<Drawer.CloseIcon />
|
|
55
|
+
</Drawer.Header>
|
|
56
|
+
<Drawer.Content>{ fields }</Drawer.Content>
|
|
57
|
+
<Drawer.Divider />
|
|
58
|
+
<Drawer.Content padding={ 0 }>{ flushTable }</Drawer.Content>
|
|
59
|
+
<Drawer.Footer>
|
|
60
|
+
<Drawer.Action variant="secondary" closes>
|
|
61
|
+
{ __( 'Cancel', 'newspack-plugin' ) }
|
|
62
|
+
</Drawer.Action>
|
|
63
|
+
<Drawer.Action variant="primary" isBusy={ inFlight } onClick={ save }>
|
|
64
|
+
{ __( 'Save', 'newspack-plugin' ) }
|
|
65
|
+
</Drawer.Action>
|
|
66
|
+
</Drawer.Footer>
|
|
67
|
+
</Drawer.Root>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Every slot is optional and nothing is composed for you: a drawer with no
|
|
71
|
+
`Drawer.Header` renders no header markup at all.
|
|
72
|
+
|
|
73
|
+
## `Drawer.Root`
|
|
74
|
+
|
|
75
|
+
Owns the panel, the close funnel and the confirmation. Everything behavioural
|
|
76
|
+
lives here.
|
|
77
|
+
|
|
78
|
+
| Prop | Type | Default | Description |
|
|
79
|
+
|------|------|---------|-------------|
|
|
80
|
+
| `children` | `React.ReactNode` | — | The slots. See the constraint under [`Drawer.Content`](#drawercontent). |
|
|
81
|
+
| `className` | `string` | — | Additional CSS class on the panel. |
|
|
82
|
+
| `confirmButtonText` | `string` | `'Discard Changes'` | Confirm label on the built-in unsaved-changes dialog. |
|
|
83
|
+
| `confirmCloseMessage` | `React.ReactNode` | `'You have unsaved changes that will be lost. Discard changes?'` | Body of the built-in unsaved-changes dialog. |
|
|
84
|
+
| `contentLabel` | `string` | — | Accessible name when the design has no visible title. Ignored when a `Drawer.Title` is rendered. |
|
|
85
|
+
| `describedBy` | `string` | — | Id of an element describing the panel, for the frame's `aria-describedby`. |
|
|
86
|
+
| `isDirty` | `boolean` | `false` | Routes every close through a confirmation. See [Closing and confirmation](#closing-and-confirmation). |
|
|
87
|
+
| `isOpen` | `boolean` | `false` | Whether the panel is showing. Keep the Root mounted either way. |
|
|
88
|
+
| `onRequestClose` | `() => void` | — | **Required.** Called once a close is confirmed. |
|
|
89
|
+
| `requestConfirm` | `( callback: () => void ) => void` | — | Delegates confirmation to a dialog you already own. |
|
|
90
|
+
| `ref` | `React.Ref< HTMLDivElement >` | — | Lands on the overlay, which is what core Modal forwards a ref to. |
|
|
91
|
+
| `size` | `'small'` \| `'medium'` \| `'large'` \| `'x-large'` \| `'full'` | `'medium'` | Panel width. An unknown value falls back to `medium` and warns in development. |
|
|
92
|
+
| `style` | `React.CSSProperties` | — | Merged into the panel's own style. |
|
|
93
|
+
|
|
94
|
+
### Sizes
|
|
95
|
+
|
|
96
|
+
| `size` | Width |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `small` | 280px |
|
|
99
|
+
| `medium` (default) | 350px |
|
|
100
|
+
| `large` | 480px |
|
|
101
|
+
| `x-large` | 640px |
|
|
102
|
+
| `full` | 100vw |
|
|
103
|
+
|
|
104
|
+
At 600px and below every size goes full width.
|
|
105
|
+
|
|
106
|
+
## `Drawer.Header`
|
|
107
|
+
|
|
108
|
+
A flex row, pinned above the body. Compose an icon, a `Drawer.Title` and a
|
|
109
|
+
`Drawer.CloseIcon` inside it; the order is yours. Takes `className` and
|
|
110
|
+
`children`.
|
|
111
|
+
|
|
112
|
+
Give a leading icon `className="newspack-drawer__icon"`, which stops it
|
|
113
|
+
shrinking and fills it with `currentcolor`.
|
|
114
|
+
|
|
115
|
+
## `Drawer.Title`
|
|
116
|
+
|
|
117
|
+
Renders an `h2` and registers its id with the Root, which wires it to the
|
|
118
|
+
panel's `aria-labelledby`. Takes `className` and `children`.
|
|
119
|
+
|
|
120
|
+
A single plain-string child also registers its text, and `Drawer.CloseIcon`
|
|
121
|
+
composes that into "Close {title}". Mixed children are not a string, so the
|
|
122
|
+
close button falls back to a bare "Close". Build the string first to keep the
|
|
123
|
+
composed label.
|
|
124
|
+
|
|
125
|
+
```jsx
|
|
126
|
+
// The close button is named "Close Edit Styles".
|
|
127
|
+
<Drawer.Title>{ __( 'Edit Styles', 'newspack-plugin' ) }</Drawer.Title>
|
|
128
|
+
|
|
129
|
+
// Interpolated, so the children are an array and the close button is
|
|
130
|
+
// named "Close".
|
|
131
|
+
<Drawer.Title>{ __( 'Edit', 'newspack-plugin' ) } { name }</Drawer.Title>
|
|
132
|
+
|
|
133
|
+
// One string child again, so the composed label is back.
|
|
134
|
+
// translators: %s: the item being edited.
|
|
135
|
+
<Drawer.Title>{ sprintf( __( 'Edit %s', 'newspack-plugin' ), name ) }</Drawer.Title>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## `Drawer.CloseIcon`
|
|
139
|
+
|
|
140
|
+
An icon-only button. Clicking it goes through the Root's close funnel, so the
|
|
141
|
+
unsaved-changes confirmation always applies.
|
|
142
|
+
|
|
143
|
+
| Prop | Type | Default | Description |
|
|
144
|
+
|------|------|---------|-------------|
|
|
145
|
+
| `className` | `string` | — | Additional CSS class. |
|
|
146
|
+
| `icon` | `JSX.Element` | `close` from `@wordpress/icons` | Icon to render. |
|
|
147
|
+
| `label` | `string` | `'Close {title}'`, or `'Close'` without one | Tooltip and accessible name. |
|
|
148
|
+
|
|
149
|
+
## `Drawer.Content`
|
|
150
|
+
|
|
151
|
+
The body. Repeatable: each `Drawer.Content` is a section, rendered as a VStack
|
|
152
|
+
with a 16px gap by default; `gap` changes it. Children space through the gap
|
|
153
|
+
alone; their own top and bottom margins are reset.
|
|
154
|
+
|
|
155
|
+
Anything can go in a section: an element, a control, a nested stack, or plain
|
|
156
|
+
text. Each child becomes a row separated by the gap, and a run of plain text and
|
|
157
|
+
string interpolations stays one row, so `Edited by { name }` reads as a sentence
|
|
158
|
+
rather than stacking. Whitespace between two elements is not a row of its own.
|
|
159
|
+
|
|
160
|
+
An element always starts a new row, including inside a sentence: `Edited by
|
|
161
|
+
<strong>{ name }</strong>` is two rows, not one. Wrap a sentence that mixes text
|
|
162
|
+
with markup in your own element and hand the section that instead.
|
|
163
|
+
|
|
164
|
+
| Prop | Type | Default | Description |
|
|
165
|
+
|------|------|---------|-------------|
|
|
166
|
+
| `children` | `React.ReactNode` | — | Section content. |
|
|
167
|
+
| `className` | `string` | — | Additional CSS class. |
|
|
168
|
+
| `gap` | `number` | `4` | Space between the section's children, on the 4px scale, as VStack's `spacing`. `4` is 16px. |
|
|
169
|
+
| `padding` | `number` | `6` | On the 4px scale, as VStack's `spacing`. `6` is 24px; `0` is a flush section that brings its own padding. |
|
|
170
|
+
|
|
171
|
+
Consecutive sections share one scroll container, so they scroll together between
|
|
172
|
+
the pinned header and footer. Sections are not self-separating: put a
|
|
173
|
+
[`Drawer.Divider`](#drawerdivider) between two of them to draw a rule. The
|
|
174
|
+
first section drops its top padding when a header precedes it, and the last
|
|
175
|
+
section drops its bottom padding when a footer follows, so the seam at each end
|
|
176
|
+
is the chrome's own 16px padding. Without that chrome the section keeps its own
|
|
177
|
+
padding and sits against the edge of the panel.
|
|
178
|
+
|
|
179
|
+
**Sections must be direct children of `Drawer.Root`.** The Root walks its direct
|
|
180
|
+
children to build that scroll container. A section nested in a fragment or any
|
|
181
|
+
other wrapper is passed straight through and never grouped, silently. Arrays are
|
|
182
|
+
fine.
|
|
183
|
+
|
|
184
|
+
```jsx
|
|
185
|
+
// Yes.
|
|
186
|
+
<Drawer.Root isOpen={ isOpen } onRequestClose={ close }>
|
|
187
|
+
<Drawer.Content>{ fields }</Drawer.Content>
|
|
188
|
+
{ hasTable && <Drawer.Content padding={ 0 }>{ table }</Drawer.Content> }
|
|
189
|
+
</Drawer.Root>
|
|
190
|
+
|
|
191
|
+
// No. The fragment is one non-Content child, so neither section is grouped.
|
|
192
|
+
<Drawer.Root isOpen={ isOpen } onRequestClose={ close }>
|
|
193
|
+
<>
|
|
194
|
+
<Drawer.Content>{ fields }</Drawer.Content>
|
|
195
|
+
<Drawer.Content padding={ 0 }>{ table }</Drawer.Content>
|
|
196
|
+
</>
|
|
197
|
+
</Drawer.Root>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Guard a conditional section on a boolean.** `null`, `undefined` and `false`
|
|
201
|
+
are dropped and leave the run intact, but `0` and `''` are not: they count as
|
|
202
|
+
non-Content children, so `{ items.length && <Drawer.Content/> }` with an empty
|
|
203
|
+
array renders a stray "0" in the drawer and splits the run in two.
|
|
204
|
+
|
|
205
|
+
```jsx
|
|
206
|
+
// Yes.
|
|
207
|
+
{ !! items.length && <Drawer.Content>{ list }</Drawer.Content> }
|
|
208
|
+
{ items.length ? <Drawer.Content>{ list }</Drawer.Content> : null }
|
|
209
|
+
|
|
210
|
+
// No. An empty array leaves a "0" in the drawer and a split body.
|
|
211
|
+
{ items.length && <Drawer.Content>{ list }</Drawer.Content> }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A child that is neither a `Drawer.Content` nor a `Drawer.Divider` splits the
|
|
215
|
+
sections around it into two scroll containers. Each container is keyed by its
|
|
216
|
+
position among the bodies, not among the children, so a drawer with one body
|
|
217
|
+
keeps it, and its scroll position, when a sibling above it is toggled. With
|
|
218
|
+
several bodies, a change that merges or splits an earlier run re-keys the ones
|
|
219
|
+
after it, which resets their scroll.
|
|
220
|
+
|
|
221
|
+
## `Drawer.Divider`
|
|
222
|
+
|
|
223
|
+
A full-width rule between two sections. It joins the run, so it sits inside the
|
|
224
|
+
same scroll container and travels with the sections rather than splitting them.
|
|
225
|
+
|
|
226
|
+
Takes `className`. Renders an `<hr>`, so assistive technology announces it as a
|
|
227
|
+
separator.
|
|
228
|
+
|
|
229
|
+
```jsx
|
|
230
|
+
<Drawer.Content>{ fields }</Drawer.Content>
|
|
231
|
+
<Drawer.Divider />
|
|
232
|
+
<Drawer.Content>{ moreFields }</Drawer.Content>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## `Drawer.Footer`
|
|
236
|
+
|
|
237
|
+
A pinned container for `Drawer.Action` elements. The layout is pure CSS on the
|
|
238
|
+
child count.
|
|
239
|
+
|
|
240
|
+
| Children | Layout |
|
|
241
|
+
|---|---|
|
|
242
|
+
| 1 | Row, the button fills it |
|
|
243
|
+
| 2 | Row, split 50/50 |
|
|
244
|
+
| 3 or more | Column, full-width buttons |
|
|
245
|
+
|
|
246
|
+
Convention, not enforced: with two actions pass the secondary first, so the
|
|
247
|
+
primary sits on the right; with three pass primary, secondary, tertiary
|
|
248
|
+
top-down.
|
|
249
|
+
|
|
250
|
+
Takes `className` and `children`.
|
|
251
|
+
|
|
252
|
+
## `Drawer.Action`
|
|
253
|
+
|
|
254
|
+
A footer button, wrapping the Newspack `Button` so `href` and router behaviour
|
|
255
|
+
work as they do elsewhere. Children are the visible label.
|
|
256
|
+
|
|
257
|
+
| Prop | Type | Default | Description |
|
|
258
|
+
|------|------|---------|-------------|
|
|
259
|
+
| `ariaLabel` | `string` | — | Extends the visible label with context. Must contain it. |
|
|
260
|
+
| `children` | `React.ReactNode` | — | The visible label. |
|
|
261
|
+
| `className` | `string` | — | Additional CSS class. |
|
|
262
|
+
| `closes` | `boolean` | `false` | Requests the drawer close through the Root's funnel after `onClick`, so the unsaved-changes confirmation applies. |
|
|
263
|
+
| `disabled` | `boolean` | — | Disables the button. |
|
|
264
|
+
| `href` | `string` | — | Renders a link instead of a button. Cannot be combined with `onClick` or `closes`. |
|
|
265
|
+
| `isBusy` | `boolean` | — | Busy styling, for a save in flight. |
|
|
266
|
+
| `isDestructive` | `boolean` | — | Destructive styling. |
|
|
267
|
+
| `onClick` | `( event?: React.MouseEvent ) => void` | — | Click handler. Receives the event. |
|
|
268
|
+
| `variant` | `'primary'` \| `'secondary'` \| `'tertiary'` \| `'link'` | — | Button variant. |
|
|
269
|
+
|
|
270
|
+
`ariaLabel` renders as a plain `aria-label`, not the WP `Button` `label` that
|
|
271
|
+
would put a tooltip on a button which already has visible text.
|
|
272
|
+
|
|
273
|
+
An action either navigates or acts. `href` with `onClick` or `closes` is a type
|
|
274
|
+
error: the underlying `Button` drops the `href` as soon as a handler is present,
|
|
275
|
+
and a link that also runs the close funnel would navigate away while the
|
|
276
|
+
unsaved-changes confirmation was still opening.
|
|
277
|
+
|
|
278
|
+
Without `closes`, an action runs the `onClick` you give it and nothing else,
|
|
279
|
+
and `isDirty` does not cover it. Give a Cancel or Close action `closes` so it
|
|
280
|
+
goes through the same funnel as `Drawer.CloseIcon`; leave it off a Save, which
|
|
281
|
+
should close by flipping `isOpen` once its work succeeds.
|
|
282
|
+
|
|
283
|
+
## Closing and confirmation
|
|
284
|
+
|
|
285
|
+
Every close route, the `Drawer.CloseIcon`, Escape and an overlay press, goes
|
|
286
|
+
through one handler in the Root. Core Modal's own close paths are switched off,
|
|
287
|
+
so nothing can close the panel behind that handler.
|
|
288
|
+
|
|
289
|
+
**`isDirty` false.** The handler calls `onRequestClose`, the parent sets `isOpen`
|
|
290
|
+
false, and the slide-out plays.
|
|
291
|
+
|
|
292
|
+
**`isDirty` true.** The handler opens the confirm dialog above the drawer, which
|
|
293
|
+
has not moved. "Discard Changes" calls `onRequestClose`, and only then does the
|
|
294
|
+
slide-out play. Cancel does nothing and the panel is still where it was. A
|
|
295
|
+
refused close never animates.
|
|
296
|
+
|
|
297
|
+
Raise `isDirty` while a save is in flight too, so an in-progress request cannot
|
|
298
|
+
be closed out from under.
|
|
299
|
+
|
|
300
|
+
```jsx
|
|
301
|
+
<Drawer.Root isOpen={ isOpen } isDirty={ isDirty || inFlight } onRequestClose={ close }>
|
|
302
|
+
{ slots }
|
|
303
|
+
</Drawer.Root>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Footer actions with `closes` are covered.** `closes` sends the action through
|
|
307
|
+
the same handler as the close icon, so a Cancel written that way gets the
|
|
308
|
+
confirmation and a Save without it does not.
|
|
309
|
+
|
|
310
|
+
```jsx
|
|
311
|
+
<Drawer.Footer>
|
|
312
|
+
<Drawer.Action variant="secondary" closes>{ __( 'Cancel', 'newspack-plugin' ) }</Drawer.Action>
|
|
313
|
+
<Drawer.Action variant="primary" onClick={ save }>{ __( 'Save', 'newspack-plugin' ) }</Drawer.Action>
|
|
314
|
+
</Drawer.Footer>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The action's own `onClick` runs *before* the handler, so its side effects survive
|
|
318
|
+
a cancelled confirmation. Keep anything that must not happen twice out of an
|
|
319
|
+
action that also closes.
|
|
320
|
+
|
|
321
|
+
**Another modal opening closes a clean panel, and leaves a dirty one alone.**
|
|
322
|
+
Core shows one modal at a time and dismisses the drawer as a sibling mounts. A
|
|
323
|
+
clean panel takes that and closes. A dirty one does not answer it at all: its own
|
|
324
|
+
confirmation would render underneath the modal that just opened, and a delegated
|
|
325
|
+
confirmation *is* the modal that triggered the dismissal. So a dirty panel stays
|
|
326
|
+
open behind the new dialog and is still there once it is dealt with.
|
|
327
|
+
|
|
328
|
+
**Inside a wizard that already runs `useUnsavedChangesDialog`, pass its
|
|
329
|
+
`requestConfirm`.** The Root then delegates and mounts no dialog of its own. Two
|
|
330
|
+
live dialogs prompt each other: the Root's cancel path calls `history.replace`,
|
|
331
|
+
which the wizard's navigation blocker catches.
|
|
332
|
+
|
|
333
|
+
**The guard's `when` has to cover everything the Root's `isDirty` covers.** A
|
|
334
|
+
delegated `requestConfirm` runs its callback immediately, with no dialog, when
|
|
335
|
+
its own `when` is false. The Root cannot see that flag, so a panel whose
|
|
336
|
+
`isDirty` is true while the guard's `when` is false closes with no prompt and
|
|
337
|
+
loses the edits. Drive both from the same state, as below, or widen `when` to
|
|
338
|
+
include the panel's. The Root warns in development when it notices a delegated
|
|
339
|
+
confirmation closing the panel without prompting.
|
|
340
|
+
|
|
341
|
+
**A delegated dialog is yours to dismiss.** The Root only ever withdraws the one
|
|
342
|
+
it owns. If the panel closes while your prompt is up — a save that lands while
|
|
343
|
+
the user is still looking at it — the prompt stays, and answering it calls
|
|
344
|
+
`onRequestClose` on a panel that has already gone. Call the hook's
|
|
345
|
+
`cancelConfirm` yourself when you close the panel out from under it. The Root
|
|
346
|
+
deliberately does not: the same dialog serves the whole wizard, and it has no way
|
|
347
|
+
to tell a prompt it raised from one your own navigation guard raised.
|
|
348
|
+
|
|
349
|
+
```jsx
|
|
350
|
+
const { confirmDialog, requestConfirm, cancelConfirm } = useUnsavedChangesDialog( { when: isDirty } );
|
|
351
|
+
|
|
352
|
+
const save = async () => {
|
|
353
|
+
await persist();
|
|
354
|
+
// The panel closes on its own here, so withdraw the prompt along with it.
|
|
355
|
+
cancelConfirm();
|
|
356
|
+
setIsOpen( false );
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
<Drawer.Root
|
|
360
|
+
isOpen={ isOpen }
|
|
361
|
+
isDirty={ isDirty }
|
|
362
|
+
requestConfirm={ requestConfirm }
|
|
363
|
+
onRequestClose={ close }
|
|
364
|
+
>
|
|
365
|
+
{ slots }
|
|
366
|
+
</Drawer.Root>
|
|
367
|
+
{ confirmDialog }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Accessibility
|
|
371
|
+
|
|
372
|
+
**Name the panel.** A `Drawer.Title` supplies `aria-labelledby`; a `contentLabel`
|
|
373
|
+
on the Root supplies `aria-label` when the design has no visible title. With
|
|
374
|
+
neither, the Root warns in development: an unnamed dialog is announced without a
|
|
375
|
+
name when focus enters it.
|
|
376
|
+
|
|
377
|
+
**Give terse actions context.** "Save" alone answers neither "save what" nor
|
|
378
|
+
"save where" for someone who lands on the button directly. `ariaLabel` extends
|
|
379
|
+
the visible text:
|
|
380
|
+
|
|
381
|
+
```jsx
|
|
382
|
+
<Drawer.Action variant="primary" ariaLabel={ __( 'Save styles', 'newspack-plugin' ) } onClick={ save }>
|
|
383
|
+
{ __( 'Save', 'newspack-plugin' ) }
|
|
384
|
+
</Drawer.Action>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
`ariaLabel` **must contain** the visible label. Voice control matches spoken
|
|
388
|
+
commands against the accessible name, so a button reading "Save" whose name is
|
|
389
|
+
"Apply changes" stops responding to "click Save" (WCAG 2.5.3, Label in Name).
|
|
390
|
+
`Drawer.Action` warns in development when the visible string is missing from
|
|
391
|
+
`ariaLabel`. Icon-only actions are not supported, since there is no visible text
|
|
392
|
+
to extend.
|
|
393
|
+
|
|
394
|
+
The rule holds per locale, and the check only sees the translated strings. The
|
|
395
|
+
two land in the POT as unrelated entries, so give the `ariaLabel` a `translators:`
|
|
396
|
+
comment naming the visible label it has to contain — otherwise the guarantee is
|
|
397
|
+
lost in any locale where they drift apart, and the warning only fires for someone
|
|
398
|
+
running that locale in development.
|
|
399
|
+
|
|
400
|
+
```jsx
|
|
401
|
+
/* translators: extended label for the Save button. Must contain the word "Save" as translated below. */
|
|
402
|
+
ariaLabel={ __( 'Save styles', 'newspack-plugin' ) }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Reduced motion.** Under `prefers-reduced-motion: reduce` the slide and the
|
|
406
|
+
scrim fade are dropped, and the panel is removed at once rather than waiting on
|
|
407
|
+
an animation that never runs.
|
|
408
|
+
|
|
409
|
+
**Scrolling without a mouse.** A scroll container that holds nothing focusable
|
|
410
|
+
cannot be reached from the keyboard in every browser, so the body takes
|
|
411
|
+
`tabindex="0"` and the "Scrollable section" label whenever it actually overflows,
|
|
412
|
+
and drops both when it does not (WCAG 2.1.1). Core Modal does this for its own
|
|
413
|
+
container, but the drawer scrolls in the body instead, leaving that one no taller
|
|
414
|
+
than its box. Overflow is measured on the body and its sections, so a section
|
|
415
|
+
that grows after mount is picked up.
|
|
416
|
+
|
|
417
|
+
## Popovers
|
|
418
|
+
|
|
419
|
+
The Root wraps its children in a `SlotFillProvider` and renders a `Popover.Slot`
|
|
420
|
+
beside them, so dropdowns and pickers inside the drawer stay visible. Without
|
|
421
|
+
both, a popover
|
|
422
|
+
portalled to the body would land in a container Modal has marked `aria-hidden`,
|
|
423
|
+
and our own `modal/style.scss` blanks popovers while a modal is open.
|
|
424
|
+
|
|
425
|
+
That provider starts a fresh registry rather than chaining to a parent one. **A
|
|
426
|
+
`Fill` rendered inside the drawer cannot reach a `Slot` outside it**: it renders
|
|
427
|
+
nothing, silently. Move the `Slot` inside the drawer.
|
|
428
|
+
|
|
429
|
+
## Outside the Root
|
|
430
|
+
|
|
431
|
+
`Drawer.Title` and `Drawer.CloseIcon` read the Root's private context and throw
|
|
432
|
+
"Drawer subcomponents must be rendered inside Drawer.Root." when they are
|
|
433
|
+
rendered anywhere else.
|
|
434
|
+
|
|
435
|
+
## On a `@wordpress/components` upgrade
|
|
436
|
+
|
|
437
|
+
The panel reaches into core Modal in three places that are internals rather than
|
|
438
|
+
a documented contract, and the peer range is a caret. Check them on a bump:
|
|
439
|
+
|
|
440
|
+
- The Root finds the frame with `.components-modal__frame` inside the node Modal
|
|
441
|
+
forwards its ref to, and uses it for focus return and `inert`. It warns in
|
|
442
|
+
development when that lookup comes back empty.
|
|
443
|
+
- `__experimentalHideHeader` suppresses core's own header. Losing it puts a
|
|
444
|
+
second header above `Drawer.Header`.
|
|
445
|
+
- `style.scss` targets `.components-modal__content` and
|
|
446
|
+
`.components-modal__children-container` for the panel's layout.
|
|
447
|
+
- `shouldCloseOnEsc={ false }`, `shouldCloseOnClickOutside={ false }` and
|
|
448
|
+
`__experimentalHideHeader` are what leave core with no close path of its own.
|
|
449
|
+
Re-enabling any of them — a header for `headerActions`, say — gives core a way
|
|
450
|
+
to close a dirty panel with no confirmation.
|
|
451
|
+
|
|
452
|
+
Core Modal is externalised to the `wp.components` runtime global at build time,
|
|
453
|
+
so the version that actually runs is the one on the publisher's WordPress, not
|
|
454
|
+
the one resolved here.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { useContext, useEffect } from '@wordpress/element';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies.
|
|
8
|
+
*/
|
|
9
|
+
import Button from '../button';
|
|
10
|
+
import { DrawerContext } from './context';
|
|
11
|
+
import type { DrawerActionProps } from './types';
|
|
12
|
+
|
|
13
|
+
const Action = ( { ariaLabel, closes = false, onClick, href, children, ...buttonProps }: DrawerActionProps ) => {
|
|
14
|
+
// Not useDrawerContext: only `closes` requires the context.
|
|
15
|
+
const context = useContext( DrawerContext );
|
|
16
|
+
|
|
17
|
+
// `href=""` is still an anchor; a null from a JS caller means no link at all.
|
|
18
|
+
const hasHref = 'string' === typeof href;
|
|
19
|
+
|
|
20
|
+
useEffect( () => {
|
|
21
|
+
if ( 'production' === process.env.NODE_ENV || ! hasHref ) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if ( onClick || closes ) {
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.warn(
|
|
27
|
+
'Drawer: an action with `href` cannot also take `onClick` or `closes`, and they are ignored. ' +
|
|
28
|
+
'A link that ran the close funnel would navigate away while the unsaved-changes confirmation was still opening.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}, [ hasHref, onClick, closes ] );
|
|
32
|
+
|
|
33
|
+
useEffect( () => {
|
|
34
|
+
if ( 'production' === process.env.NODE_ENV ) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if ( typeof children === 'string' && ariaLabel && ! ariaLabel.toLowerCase().includes( children.toLowerCase() ) ) {
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
console.warn(
|
|
40
|
+
`Drawer: action ariaLabel "${ ariaLabel }" does not contain its visible label "${ children }". ` +
|
|
41
|
+
'Voice control matches spoken commands against the accessible name (WCAG 2.5.3, Label in Name), ' +
|
|
42
|
+
'so ariaLabel must extend the visible text rather than replace it.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}, [ ariaLabel, children ] );
|
|
46
|
+
|
|
47
|
+
const handleClick = ( event?: React.MouseEvent< HTMLElement > ) => {
|
|
48
|
+
onClick?.( event );
|
|
49
|
+
if ( closes ) {
|
|
50
|
+
if ( ! context ) {
|
|
51
|
+
throw new Error( 'Drawer subcomponents must be rendered inside Drawer.Root.' );
|
|
52
|
+
}
|
|
53
|
+
context.requestClose();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Button swallows `href` whenever an onClick is present, so a link supplies none.
|
|
58
|
+
const handlesClick = ! hasHref && ( !! onClick || closes );
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Button aria-label={ ariaLabel } href={ hasHref ? href : undefined } onClick={ handlesClick ? handleClick : undefined } { ...buttonProps }>
|
|
62
|
+
{ children }
|
|
63
|
+
</Button>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default Action;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
5
|
+
import { useEffect, useState } from '@wordpress/element';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Internal dependencies.
|
|
10
|
+
*/
|
|
11
|
+
import type { DrawerBodyProps } from './types';
|
|
12
|
+
|
|
13
|
+
// Core tab-stops its own container once it overflows; the drawer scrolls here.
|
|
14
|
+
const Body = ( { children }: DrawerBodyProps ) => {
|
|
15
|
+
const [ node, setNode ] = useState< HTMLElement | null >( null );
|
|
16
|
+
const [ isScrollable, setIsScrollable ] = useState( false );
|
|
17
|
+
|
|
18
|
+
useEffect( () => {
|
|
19
|
+
if ( ! node ) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const measure = () => setIsScrollable( node.scrollHeight > node.clientHeight );
|
|
23
|
+
measure();
|
|
24
|
+
if ( ! window.ResizeObserver ) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const resize = new ResizeObserver( measure );
|
|
28
|
+
const observeChildren = () => {
|
|
29
|
+
resize.disconnect();
|
|
30
|
+
resize.observe( node );
|
|
31
|
+
// Border box: a section's padding rides on an inline custom property.
|
|
32
|
+
Array.from( node.children ).forEach( child => resize.observe( child, { box: 'border-box' } ) );
|
|
33
|
+
measure();
|
|
34
|
+
};
|
|
35
|
+
observeChildren();
|
|
36
|
+
const mutation = new MutationObserver( observeChildren );
|
|
37
|
+
mutation.observe( node, { childList: true } );
|
|
38
|
+
return () => {
|
|
39
|
+
resize.disconnect();
|
|
40
|
+
mutation.disconnect();
|
|
41
|
+
};
|
|
42
|
+
}, [ node ] );
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<VStack
|
|
46
|
+
ref={ setNode }
|
|
47
|
+
className="newspack-drawer__body"
|
|
48
|
+
spacing={ 0 }
|
|
49
|
+
justify="flex-start"
|
|
50
|
+
// A bare div is role=generic, where ARIA prohibits an author name.
|
|
51
|
+
role={ isScrollable ? 'group' : undefined }
|
|
52
|
+
tabIndex={ isScrollable ? 0 : undefined }
|
|
53
|
+
aria-label={ isScrollable ? __( 'Scrollable section', 'newspack-plugin' ) : undefined }
|
|
54
|
+
>
|
|
55
|
+
{ children }
|
|
56
|
+
</VStack>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default Body;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { close } from '@wordpress/icons';
|
|
5
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* External dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import classnames from 'classnames';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies.
|
|
14
|
+
*/
|
|
15
|
+
import Button from '../button';
|
|
16
|
+
import { useDrawerContext } from './context';
|
|
17
|
+
import type { DrawerCloseIconProps } from './types';
|
|
18
|
+
|
|
19
|
+
const CloseIcon = ( { label, icon = close, className }: DrawerCloseIconProps ) => {
|
|
20
|
+
const { requestClose, title } = useDrawerContext();
|
|
21
|
+
const defaultLabel = title?.text
|
|
22
|
+
? // translators: %s: the drawer's title.
|
|
23
|
+
sprintf( __( 'Close %s', 'newspack-plugin' ), title.text )
|
|
24
|
+
: __( 'Close', 'newspack-plugin' );
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Button
|
|
28
|
+
className={ classnames( 'newspack-drawer__dismiss', className ) }
|
|
29
|
+
icon={ icon }
|
|
30
|
+
size="small"
|
|
31
|
+
label={ label || defaultLabel }
|
|
32
|
+
onClick={ requestClose }
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default CloseIcon;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
5
|
+
import { Children, isValidElement } from '@wordpress/element';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* External dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import classnames from 'classnames';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies.
|
|
14
|
+
*/
|
|
15
|
+
import type { DrawerContentProps } from './types';
|
|
16
|
+
|
|
17
|
+
// VStack drops non-elements, a lone string aside. A run shares one wrapper so
|
|
18
|
+
// `Edited by { name }` is one row, not three.
|
|
19
|
+
const asRows = ( children: React.ReactNode ) => {
|
|
20
|
+
if ( 'string' === typeof children ) {
|
|
21
|
+
return children;
|
|
22
|
+
}
|
|
23
|
+
const rows: React.ReactNode[] = [];
|
|
24
|
+
let text: React.ReactNode[] = [];
|
|
25
|
+
const flushText = () => {
|
|
26
|
+
if ( text.some( part => '' !== String( part ).trim() ) ) {
|
|
27
|
+
rows.push( <div key={ `text-${ rows.length }` }>{ text }</div> );
|
|
28
|
+
}
|
|
29
|
+
text = [];
|
|
30
|
+
};
|
|
31
|
+
Children.toArray( children ).forEach( child => {
|
|
32
|
+
if ( isValidElement( child ) ) {
|
|
33
|
+
flushText();
|
|
34
|
+
rows.push( child );
|
|
35
|
+
} else {
|
|
36
|
+
text.push( child );
|
|
37
|
+
}
|
|
38
|
+
} );
|
|
39
|
+
flushText();
|
|
40
|
+
return rows;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const Content = ( { padding = 6, gap = 4, className, children }: DrawerContentProps ) => (
|
|
44
|
+
<VStack
|
|
45
|
+
className={ classnames( 'newspack-drawer__content', className ) }
|
|
46
|
+
spacing={ gap }
|
|
47
|
+
// A custom property, not inline padding, so the stylesheet's seam rules win.
|
|
48
|
+
style={ { '--newspack-drawer-content-padding': `${ padding * 4 }px` } as React.CSSProperties }
|
|
49
|
+
>
|
|
50
|
+
{ asRows( children ) }
|
|
51
|
+
</VStack>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export default Content;
|