Skip to content

Commit 32dbd20

Browse files
authored
docs(FR-2897): clarify modal footer prop-first guidance in react-modal-drawer skill (#7420)
1 parent 67278e6 commit 32dbd20

1 file changed

Lines changed: 90 additions & 5 deletions

File tree

  • .claude/skills/react-modal-drawer

.claude/skills/react-modal-drawer/SKILL.md

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,58 @@ Reusable confirmation helpers that exist:
206206
- `BAIConfirmModalWithInput` — confirm by typing a token
207207
- `BAIDeleteConfirmModal` — dangerous delete flow with double-check
208208

209-
## 6. Modal Footer Layout
209+
## 6. Modal Footer: prefer built-in props, custom `footer` is a last resort
210+
211+
**Default: use the modal's built-in OK/Cancel props.** `BAIModal` (and antd
212+
`Modal`) already render a standard OK + Cancel footer wired to `onOk` /
213+
`onCancel`. Customize it through props before reaching for a custom `footer`:
214+
215+
| Need | Prop |
216+
|---|---|
217+
| Submit handler | `onOk` (async supported) |
218+
| Cancel handler | `onCancel` |
219+
| Submit label | `okText` |
220+
| Cancel label | `cancelText` |
221+
| Submit button loading | `confirmLoading` — bind to the mutation's `isInFlight` / `isPending` |
222+
| Submit button danger / disabled / icon | `okButtonProps` |
223+
| Cancel button styling | `cancelButtonProps` |
224+
| Hide a button | `okButtonProps={{ style: { display: 'none' } }}` or `cancelButtonProps={{ ... }}` |
210225

211-
Use `BAIFlex` for footer layout, not `<Space>`:
226+
```tsx
227+
<BAIModal
228+
open={open}
229+
title={t('data.CreateFolder')}
230+
okText={t('data.Create')}
231+
confirmLoading={isInFlightCreate}
232+
onOk={async () => {
233+
await form.validateFields();
234+
await commitCreate({ variables: form.getFieldsValue() });
235+
onRequestClose();
236+
}}
237+
onCancel={() => onRequestClose()}
238+
>
239+
{/* body */}
240+
</BAIModal>
241+
```
242+
243+
This keeps button placement, sizing, spacing, and i18n consistent with every
244+
other modal in the app, and `confirmLoading` handles the pending state without
245+
you wiring an action button by hand.
246+
247+
### When to use a custom `footer` (last resort)
248+
249+
Only override `footer` when the built-in props genuinely cannot express the
250+
layout, for example:
251+
252+
- A **third button** beyond OK/Cancel (e.g. a `Reset` on the left).
253+
- A **non-standard layout** (e.g. left-aligned destructive action separated
254+
from the right-aligned confirm pair).
255+
- A footer that must include **non-button content** (a hint, a checkbox like
256+
"auto-activate after create", a status badge).
257+
258+
If you do override `footer`, use `BAIFlex` for layout (not `<Space>`), keep
259+
the primary action rightmost, and use `BAIButton.action` on the submit button
260+
so loading state is automatic. Never pair `action` with `onClick`.
212261

213262
```tsx
214263
footer={
@@ -231,8 +280,42 @@ footer={
231280
}
232281
```
233282

234-
The primary submit button always uses `BAIButton.action` so loading state is
235-
automatic. Never pair `action` with `onClick`.
283+
### Anti-pattern: re-implementing the standard footer
284+
285+
Don't hand-roll a `BAIFlex justify="end"` with Cancel + primary buttons inside
286+
the modal body (or as a custom `footer={…}`) when the built-in `onOk` /
287+
`onCancel` / `okText` / `confirmLoading` props cover it. Even if the submit
288+
button uses `BAIButton.action` (so it has a per-click loading state), bypassing
289+
the modal's footer slot still loses the standard footer semantics: position,
290+
sizing, gap, i18n alignment with every other modal, and the
291+
modal-level `confirmLoading` signal that callers expect to drive from the
292+
mutation's `isInFlight` / `isPending`. A modal that looks correct is still
293+
diverging from the project's footer contract.
294+
295+
```tsx
296+
// ❌ Wrong — re-implements the standard footer inside the body
297+
<BAIModal open={open} title={t('...')} footer={null}>
298+
<FormContent />
299+
<BAIFlex justify="end" gap="sm">
300+
<BAIButton onClick={onRequestClose}>{t('button.Cancel')}</BAIButton>
301+
<BAIButton type="primary" action={handleDeploy}>
302+
{t('modelStore.Deploy')}
303+
</BAIButton>
304+
</BAIFlex>
305+
</BAIModal>
306+
307+
// ✅ Right — props give you the same footer with confirmLoading for free
308+
<BAIModal
309+
open={open}
310+
title={t('...')}
311+
okText={t('modelStore.Deploy')}
312+
confirmLoading={isInFlightDeploy}
313+
onOk={handleDeploy}
314+
onCancel={onRequestClose}
315+
>
316+
<FormContent />
317+
</BAIModal>
318+
```
236319

237320
## 7. Loading Skeleton While Data Not Ready
238321

@@ -280,5 +363,7 @@ Avoid `useEffect`-driven coordination between sibling modals. Instead:
280363
- [ ] Primary submit button uses `BAIButton.action` (not `loading={…}`).
281364
- [ ] `onRequestClose` convention used instead of split `onOk`/`onCancel` when no distinct success-vs-cancel path is needed.
282365
- [ ] Simple confirmations use `modal.confirm()` from `App.useApp()`, not an inline `<Modal>`.
283-
- [ ] Footer uses `BAIFlex`, not `<Space>`.
366+
- [ ] OK / Cancel are wired through `onOk` / `onCancel` / `okText` / `cancelText` / `okButtonProps` props — custom `footer` only when those genuinely cannot express the layout (extra button, non-button content).
367+
- [ ] Submit pending state goes through `confirmLoading` (bound to the mutation's `isInFlight` / `isPending`), not a hand-rolled `loading` button.
368+
- [ ] When a custom `footer` is justified, it uses `BAIFlex` (not `<Space>`) and the submit button uses `BAIButton.action`.
284369
- [ ] No `useEffect` chains between parent and modal — prefer lifted state + `onRequestClose`.

0 commit comments

Comments
 (0)