docs: record overlay ownership and focus lessons (#44126)

This commit is contained in:
yyh
2026-10-15 01:20:14 +00:00
committed by GitHub
parent be814ee0e0
commit 69629bd7b7
4 changed files with 10 additions and 1 deletions
@@ -16,10 +16,12 @@ Read this document when a change involves application hotkeys, focus, dialogs, m
## Secondary Surfaces
- Follow the [overlay contract] for primitive choice and shared mechanics. The nearest consumer `AGENTS.md` owns application-specific composite reuse policy.
- Separate behavior ownership from placement ownership: the action may own trigger, open state, and menu content while the caller owns slots, offsets, and alignment.
- Compose Root, Trigger, and content at the entry that owns the business behavior, so the whole anatomy is visible there. A shared layer exports content and state parts; it does not take a trigger prop or wrap the Root and hide the content it renders. The caller owns slots, offsets, and alignment.
- Keep menu and dialog surfaces as siblings when a menu command opens a dialog. Mount the dialog outside popup content.
- Use the [overlay contract] to determine content lifetime and preserve the Root's closing lifecycle. Follow [state ownership] for draft placement and semantic identity; portal placement alone does not locate the state owner.
- Place query subscriptions and mutation observers at the owner whose lifetime matches when they should run. Mounted-session work may belong inside content; work that must start or stop exactly with `open` needs an explicit open-state condition.
- Data inside a surface follows its owner's mount, not the `open` flag. A query declared in popup content under a Suspense boundary takes the content lifetime; a result the surface displays stays with its owner until that owner unmounts.
- Anything read while a surface closes, such as the return-focus target, must come from an owner that outlives the close. The element that opened the surface may unmount with its data, and a per-call mutation callback is dropped when its component unmounts.
- Prefer primitive-owned open state unless another owner must observe or coordinate it. Analytics callbacks and local cleanup alone do not require a controlled root.
[overlay contract]: ../../../../packages/dify-ui/docs/overlays.md
@@ -39,6 +39,7 @@ Read this document when adding, moving, splitting, or refactoring React componen
- Choose component declaration and export forms from the actual component contract, framework requirements, and enforced package rules. Existing style is context, not authority; do not rewrite unaffected code solely to normalize `FC`, `function`, arrow-function, named-export, or default-export forms.
- Use API-generated or API-returned types at component boundaries. Keep one-off UI refinements and conversions beside their owner.
- Derive prop types from their source instead of writing them by hand. A prop that lands on a primitive part is typed from that part, not from a neighbouring part.
- Name props and converted data after their domain/API role, preserving traceability to the original contract.
- Preserve domain value types for selections. Do not widen enums, unions, booleans, numbers, objects, or nullable values to `string` before a real boundary requires it.
- Avoid generic `common.tsx` buckets and aliases that only rename another type. Name files, values, and public types after their domain role.
+2
View File
@@ -66,6 +66,8 @@ clipped, `display: none` or unmounted, and the shared positioner class turns tha
- Use `Dialog` for modal content that needs focus containment and scroll locking.
- Use `AlertDialog` only for a destructive or must-confirm decision requiring an explicit answer.
It blocks outside-press dismissal but still closes on Escape; cancel that close in
`onOpenChange` when the answer must be explicit.
- Use `Drawer` for side-panel interactions that follow the drawer model.
- Use `DropdownMenu` for button-triggered action lists and `ContextMenu` for context actions.
A long action list filters with `DropdownMenuFilterProvider`; choosing a value is
+4
View File
@@ -54,6 +54,10 @@ pending state only to `loading`; keep independent availability conditions in `di
Pass `focusableWhenDisabled={false}` only when a loading button should opt into native disabled
behavior and may leave the tab order.
Pass `focusableWhenDisabled` on a `disabled` button only when focus must still land on it. That
holds for the return target of a closing surface, and for a control whose tooltip explains why it
is unavailable.
### Accessible loading feedback
The spinner is decorative. Keep a non-empty visible label throughout loading. If the visible label