diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index 47d021b3cfd31..a351692a92ad7 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -132,7 +132,11 @@ work. tooltip pattern, so content too tall for the viewport scrolls inside it with no keyboard path; its full content stays available to assistive technology through the trigger's accessible description, and long or essential content - belongs in a HoverCard or Popover. + belongs in a HoverCard or Popover. A tooltip that labels an action trigger + whose accessible name already carries its text is not a separate + interaction, so a tap on that trigger performs the action and does not open + the tooltip; information that lives only in a hint is reachable on touch + through `pressTrigger="toggle"` (`component:Tooltip` DEC-1). - **INV5 — Existing state semantics win.** Disabled controls do not become interactive because of modality. Read-only and busy controls preserve their documented focus and activation behavior. @@ -303,7 +307,7 @@ reachability, newer-intent precedence, and the evidence/scope guard were approve | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | INV1, INV2 | Programmatic-focus cases after keyboard and pointer input in representative component tests | Programmatic focus invents a modality, or pointer history paints the keyboard indicator | | INV3 | Direct-owner, wrapper-owner, replaceable-indicator, and nested-action cases, including a Token inside an Item and a control inside a card; evidence gap for the shared-module cases: the module does not exist yet | No indicator, two indicators, replacement content that can opt out, or a component that writes the focus-owner attribute or selector itself | -| INV4 | Keyboard, mouse, touch, and pen browser paths where applicable | Hover-only behavior, sticky touch hover, or unreachable action | +| INV4 | Keyboard, mouse, touch, and pen browser paths where applicable; tap cases on an action trigger with a label tooltip and on a hint-only trigger with `pressTrigger="toggle"` | Hover-only behavior, sticky touch hover, unreachable action, a tap on an action trigger that opens its label tooltip, or hint-only information with no touch path | | INV5 | Disabled, busy, and read-only DOM and activation assertions | Inactive control reacts, eligible control loses focus, or state changes with modality | | INV6 | Public API review and representative source inspection | A component adds caller-controlled modality without a separate API decision | | INV7 | Rendered-browser affected-state checks across geometry, clipping, scrolling, hit testing, built-in and custom-renderer paths | At an offered interaction point one modality loses every essential path, or evidence substitutes class equality for reachability/perceptibility | diff --git a/docs/families/overlay-dismissal.md b/docs/families/overlay-dismissal.md index e270e45e11662..af0a4093121c5 100644 --- a/docs/families/overlay-dismissal.md +++ b/docs/families/overlay-dismissal.md @@ -193,9 +193,10 @@ this record's current membership snapshot must be updated with it. among the registered present members of the document it arrives in, and FR2 and FR3 apply within that document, so a host and the members nested in it keep one order wherever they render. A member of another document is never a - candidate. A Tooltip never wins or blocks an Escape press against another - member (`spec:AST-003` FR4): Escape closes a Tooltip only when it is the only - present member in its document. The stack listens once in each document that holds a + candidate. An unpinned Tooltip never wins or blocks an Escape press against + another member, and a Tooltip or HoverCard pinned open by a press takes Escape + first (`spec:AST-003` FR4, DEC-6): Escape closes an unpinned Tooltip only when + it is the only present member in its document. The stack listens once in each document that holds a member and stops when its last member leaves. - **FR9 — Focus returns in the opener's document.** Closing a trigger-anchored member returns focus, when it would otherwise be lost, to the member's return @@ -220,35 +221,35 @@ this record's current membership snapshot must be updated with it. ## Representative matrix -| Member and state | Shared invariant | Deliberate variation | -| ------------------------------------------------------ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -| Dialog inside Dialog | Inner dialog handles the first request | Dialog supplies native modality and focus behavior | -| Popover inside Dialog | Popover handles the first Escape | Popover supplies anchoring, portal placement, and focus policy | -| Registered descendant inside CommandPalette | Descendant handles the first Escape | CommandPalette inherits Dialog's shared stack participation | -| Tooltip or HoverCard inside Dialog, in page | An open HoverCard handles the first Escape; over a Tooltip, the Dialog takes it | Hover families decide when they are present and how controlled state responds | -| Lightbox or MobileNav above a required Dialog | Top member handles the request | Required Dialog uses `block` only when it is topmost | -| Required Dialog alone | Request is consumed without closing | Dialog purpose selects `block` behavior | -| Self-owned input popup | Popup participates while present | Input owns selection, focus, open state, positioning, and outside dismissal | -| Two unrelated registered surfaces | Later active-cycle registration is topmost | Neither surface is treated as nested | -| Platform close request sent to a lower Dialog | Lower Dialog stays open | The platform targets a host; the shared check decides whether it may close | -| Popover in an iframe, Dialog in the page | Request goes to its own document's top member | The app renders into the second document through a portal | -| Popover inside Dialog in an iframe | Popover handles the iframe's first request | Both register their own elements, so they rank in the iframe's order | -| Tooltip or HoverCard inside Dialog, in iframe | An open HoverCard handles the first Escape there; over a Tooltip, the Dialog takes it | A Tooltip never wins or blocks Escape against another member (`spec:AST-003` FR4) | -| ContextMenu in a Picture-in-Picture window | Escape closes it; focus returns to its opener there | The menu opens where the person pressed (FR8, FR9) | -| BottomSheet or Lightbox in a Picture-in-Picture window | Escape closes it; focus returns to its return target there | The surface covers that window (FR8, FR9) | -| BottomSheet or Lightbox in an embedded iframe | Not supported | A surface that covers its window works only where the window is the app (FR8) | -| Drawer or MobileNav | Page only | Page-level surfaces stay in the page's document (FR8) | +| Member and state | Shared invariant | Deliberate variation | +| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Dialog inside Dialog | Inner dialog handles the first request | Dialog supplies native modality and focus behavior | +| Popover inside Dialog | Popover handles the first Escape | Popover supplies anchoring, portal placement, and focus policy | +| Registered descendant inside CommandPalette | Descendant handles the first Escape | CommandPalette inherits Dialog's shared stack participation | +| Tooltip or HoverCard inside Dialog, in page | An open HoverCard handles the first Escape; over an unpinned Tooltip, the Dialog takes it; a pinned Tooltip takes it first | Hover families decide when they are present and how controlled state responds | +| Lightbox or MobileNav above a required Dialog | Top member handles the request | Required Dialog uses `block` only when it is topmost | +| Required Dialog alone | Request is consumed without closing | Dialog purpose selects `block` behavior | +| Self-owned input popup | Popup participates while present | Input owns selection, focus, open state, positioning, and outside dismissal | +| Two unrelated registered surfaces | Later active-cycle registration is topmost | Neither surface is treated as nested | +| Platform close request sent to a lower Dialog | Lower Dialog stays open | The platform targets a host; the shared check decides whether it may close | +| Popover in an iframe, Dialog in the page | Request goes to its own document's top member | The app renders into the second document through a portal | +| Popover inside Dialog in an iframe | Popover handles the iframe's first request | Both register their own elements, so they rank in the iframe's order | +| Tooltip or HoverCard inside Dialog, in iframe | An open HoverCard handles the first Escape there; over an unpinned Tooltip, the Dialog takes it; a pinned Tooltip takes it first | An unpinned Tooltip never wins or blocks Escape against another member; a pinned Tooltip or HoverCard takes it first (`spec:AST-003` FR4, DEC-6) | +| ContextMenu in a Picture-in-Picture window | Escape closes it; focus returns to its opener there | The menu opens where the person pressed (FR8, FR9) | +| BottomSheet or Lightbox in a Picture-in-Picture window | Escape closes it; focus returns to its return target there | The surface covers that window (FR8, FR9) | +| BottomSheet or Lightbox in an embedded iframe | Not supported | A surface that covers its window works only where the window is the app (FR8) | +| Drawer or MobileNav | Page only | Page-level surfaces stay in the page's document (FR8) | ## Adoption and exceptions -| Components or surface | Adoption | Current deviation or limitation | -| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Dialog, AlertDialog, Popover, MobileNav, Drawer, CommandPalette | shared owner; CommandPalette composes Dialog | none | -| Tooltip, HoverCard | shared owner with DOM presence reporting | Neither provides nesting depth to descendant layers. Implementation gap: a Tooltip consumes Escape when it is the topmost layer, so the layer beneath it stays open for that press, and a Tooltip in another document is never closed by Escape (`component:Tooltip` FR6); FR8 gives that press to the layer beneath and closes a lone Tooltip in its own document. HoverCard reads its presence from the page's document, so in another document it takes no request (FR8) and returns no focus there (FR9) | -| Focus traps with `onEscape` | shared owner through `useFocusTrap` | They provide DOM containment, not descendant depth | -| BreadcrumbItem, ChatComposerInput, ComplexSelector, DateInput, DateRangeInput, DateTimeInput, Selector, MultiSelector, PowerSearch, BaseTypeahead, Typeahead, Tokenizer, SideNavHeading, SideNavItem, TabMenu, TopNavHeading, TopNavMenu, TopNavMegaMenu, Table, Lab TourStep, Lab ChatEmojiPicker | shared owner through `usePopover` or a composed Popover owner | Table filtering owns controlled Popover state and discards its draft on close; TourStep routes Popover close to the Tour; ChatEmojiPicker owns controlled Popover state | -| DropdownMenu root, MoreMenu, ContextMenu, Selector, MultiSelector, TopNavHeading, TopNavMenu, TopNavMegaMenu, SideNavHeading, SideNavItem, BottomSheet, BottomSheetSwitcher, Lightbox | shared owner | Implementation gap: in another document (for BottomSheet, BottomSheetSwitcher and Lightbox, a Picture-in-Picture window), each decides focus return from the page's document and last input, so focus returns to the frame element or the page, or without the ring the last input in its document calls for (FR9) | -| DropdownMenuSubMenu, PowerSearchEditPopover | local only | Must migrate from component-specific listeners or registries to the shared owner | +| Components or surface | Adoption | Current deviation or limitation | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Dialog, AlertDialog, Popover, MobileNav, Drawer, CommandPalette | shared owner; CommandPalette composes Dialog | none | +| Tooltip, HoverCard | shared owner with DOM presence reporting | Neither provides nesting depth to descendant layers. Implementation gap: a Tooltip consumes Escape when it is the topmost layer, so the layer beneath it stays open for that press, and a Tooltip in another document is never closed by Escape (`component:Tooltip` FR6); FR8 gives that press to the layer beneath and closes a lone Tooltip in its own document. A Tooltip or HoverCard pinned open by a press takes Escape first (`spec:AST-003` FR4, DEC-6); implementation gap: the pin is not built (`component:Tooltip` FR9, `component:HoverCard` FR9). HoverCard reads its presence from the page's document, so in another document it takes no request (FR8) and returns no focus there (FR9) | +| Focus traps with `onEscape` | shared owner through `useFocusTrap` | They provide DOM containment, not descendant depth | +| BreadcrumbItem, ChatComposerInput, ComplexSelector, DateInput, DateRangeInput, DateTimeInput, Selector, MultiSelector, PowerSearch, BaseTypeahead, Typeahead, Tokenizer, SideNavHeading, SideNavItem, TabMenu, TopNavHeading, TopNavMenu, TopNavMegaMenu, Table, Lab TourStep, Lab ChatEmojiPicker | shared owner through `usePopover` or a composed Popover owner | Table filtering owns controlled Popover state and discards its draft on close; TourStep routes Popover close to the Tour; ChatEmojiPicker owns controlled Popover state | +| DropdownMenu root, MoreMenu, ContextMenu, Selector, MultiSelector, TopNavHeading, TopNavMenu, TopNavMegaMenu, SideNavHeading, SideNavItem, BottomSheet, BottomSheetSwitcher, Lightbox | shared owner | Implementation gap: in another document (for BottomSheet, BottomSheetSwitcher and Lightbox, a Picture-in-Picture window), each decides focus return from the page's document and last input, so focus returns to the frame element or the page, or without the ring the last input in its document calls for (FR9) | +| DropdownMenuSubMenu, PowerSearchEditPopover | local only | Must migrate from component-specific listeners or registries to the shared owner | The rows marked partial or local are current adoption gaps against FR1, and the rows naming an implementation gap are gaps against FR8 or FR9. They are not @@ -259,18 +260,18 @@ modality, positioning, outside-dismissal, and open-state contracts. ## Verification map -| Contract | Verification | Representative members and states | Mutation or failure expectation | -| ------------------ | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | -| FR1, FR2, FR4, FR5 | `useLayerDismissal.test.tsx`; `BottomSheetSwitcher.test.tsx`; `ContextMenu.test.tsx`; `CommandPalette.test.tsx` | synthetic provider depth, same-DOM containment, non-modal switcher nesting, ContextMenu nested layers, CommandPalette descendant nesting, unrelated, blocking, content-handled, present/absent | Removing depth, presence filtering, or event deferral sends Escape to the wrong surface | -| FR2, FR3, FR4, FR6 | `layerDismissalInvariants.test.tsx` | nested same-DOM Dialogs, Lightbox over Dialog, Dialog in BottomSheet, platform cancel, IME | One request closes two surfaces, closes a host before its child, or dismisses during composition | -| FR1, FR2, FR3, FR7 | `layerDismissalFamilies.test.tsx` | Dialog, Lightbox, MobileNav, BottomSheet, Tooltip, HoverCard; controlled and blocking states | A family bypasses the stack, a lower member handles a request, or controlled ownership is ignored | -| FR1 | `useFocusTrap.test.tsx` | active, nested, and deactivated traps with `onEscape` | A dismissible trap stays off the shared stack or nested traps respond together | -| FR8 | `useLayerDismissal.test.tsx`; `Popover.test.tsx` | members in the page and a same-origin iframe; a Popover alone and inside a Dialog, in an iframe | A press in one document dismisses a member of another, or a host closes over its nested layer | -| FR8 | `PopoverSecondDocument.a11y.chromium.spec.ts` | a Popover in an iframe and in a Picture-in-Picture window, alone and with one open in the page | A press in the second document misses its own layer or dismisses the page's | -| FR9 | `DialogSecondDocument.a11y.browser.spec.ts` | a Dialog in an iframe and in a Picture-in-Picture window, closed by Escape or a control inside it, and a Dialog in an iframe opened from the page | Focus lands outside the opener's document, or its ring does not follow the closing input | -| FR8, FR9 | Real-browser second-document checks; implementation gap: only Dialog and Popover have them | Dialog, Popover, DropdownMenu, MoreMenu, ContextMenu, Selector, MultiSelector, TopNavHeading, TopNavMenu, TopNavMegaMenu, SideNavHeading, SideNavItem, Tooltip and HoverCard, each in a same-origin iframe and in a Picture-in-Picture window: opens and takes Escape in the document it renders in (a Tooltip only when no other member is present), and returns focus in the opener's document | The member opens or ranks in another document, or focus lands on the frame element, the page, or the body | -| FR8, FR9 | Real-browser second-document checks; implementation gap: none has them | BottomSheet, BottomSheetSwitcher, and Lightbox in a Picture-in-Picture window: opens, takes Escape, and returns focus to its return target there | The surface ranks in the page, or focus lands on the page or the body | -| FR8 | Source check over Drawer and MobileNav | Drawer and MobileNav rendered from a control in a second document | A page-level surface moves into the second document | +| Contract | Verification | Representative members and states | Mutation or failure expectation | +| ------------------ | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1, FR2, FR4, FR5 | `useLayerDismissal.test.tsx`; `BottomSheetSwitcher.test.tsx`; `ContextMenu.test.tsx`; `CommandPalette.test.tsx` | synthetic provider depth, same-DOM containment, non-modal switcher nesting, ContextMenu nested layers, CommandPalette descendant nesting, unrelated, blocking, content-handled, present/absent | Removing depth, presence filtering, or event deferral sends Escape to the wrong surface | +| FR2, FR3, FR4, FR6 | `layerDismissalInvariants.test.tsx` | nested same-DOM Dialogs, Lightbox over Dialog, Dialog in BottomSheet, platform cancel, IME | One request closes two surfaces, closes a host before its child, or dismisses during composition | +| FR1, FR2, FR3, FR7 | `layerDismissalFamilies.test.tsx` | Dialog, Lightbox, MobileNav, BottomSheet, Tooltip, HoverCard; controlled and blocking states | A family bypasses the stack, a lower member handles a request, or controlled ownership is ignored | +| FR1 | `useFocusTrap.test.tsx` | active, nested, and deactivated traps with `onEscape` | A dismissible trap stays off the shared stack or nested traps respond together | +| FR8 | `useLayerDismissal.test.tsx`; `Popover.test.tsx` | members in the page and a same-origin iframe; a Popover alone and inside a Dialog, in an iframe | A press in one document dismisses a member of another, or a host closes over its nested layer | +| FR8 | `PopoverSecondDocument.a11y.chromium.spec.ts` | a Popover in an iframe and in a Picture-in-Picture window, alone and with one open in the page | A press in the second document misses its own layer or dismisses the page's | +| FR9 | `DialogSecondDocument.a11y.browser.spec.ts` | a Dialog in an iframe and in a Picture-in-Picture window, closed by Escape or a control inside it, and a Dialog in an iframe opened from the page | Focus lands outside the opener's document, or its ring does not follow the closing input | +| FR8, FR9 | Real-browser second-document checks; implementation gap: only Dialog and Popover have them | Dialog, Popover, DropdownMenu, MoreMenu, ContextMenu, Selector, MultiSelector, TopNavHeading, TopNavMenu, TopNavMegaMenu, SideNavHeading, SideNavItem, Tooltip and HoverCard, each in a same-origin iframe and in a Picture-in-Picture window: opens and takes Escape in the document it renders in (an unpinned Tooltip only when no other member is present; a pinned Tooltip or HoverCard before the member beneath it), and returns focus in the opener's document | The member opens or ranks in another document, a pinned Tooltip lets Escape reach the member beneath it, or focus lands on the frame element, the page, or the body | +| FR8, FR9 | Real-browser second-document checks; implementation gap: none has them | BottomSheet, BottomSheetSwitcher, and Lightbox in a Picture-in-Picture window: opens, takes Escape, and returns focus to its return target there | The surface ranks in the page, or focus lands on the page or the body | +| FR8 | Source check over Drawer and MobileNav | Drawer and MobileNav rendered from a control in a second document | A page-level surface moves into the second document | ## Decision links diff --git a/docs/specs/AST-003/spec.md b/docs/specs/AST-003/spec.md index ef1511eb20944..982b01e6ae246 100644 --- a/docs/specs/AST-003/spec.md +++ b/docs/specs/AST-003/spec.md @@ -29,7 +29,7 @@ affects_consumer_docs: [] { "scope": "global", "triggers": { - "layering": ["FR10", "DEC-1", "DEC-2", "DEC-3", "DEC-5"] + "layering": ["FR4", "FR10", "DEC-1", "DEC-2", "DEC-3", "DEC-5", "DEC-6"] } } ``` @@ -69,9 +69,12 @@ browsers receive a usable reduced fallback, not equivalent behavior. - **FR3 — Blocking owners consume without closing.** A blocking owner prevents a channel from reaching an owner behind it. Required Dialog is the representative Escape/platform case. -- **FR4 — Passive dependents never shield owners.** Tooltip is passive. It does - not win or block dismissal and disappears as a consequence when its associated - owner or trigger interaction closes. +- **FR4 — Passive dependents never shield owners.** An unpinned Tooltip is + passive. It does not win or block dismissal and disappears as a consequence + when its associated owner or trigger interaction closes. A Tooltip or + HoverCard that a press pinned open (`component:Tooltip` FR9, + `component:HoverCard` FR9) is an active owner: it takes Escape first and + closes, and the owner beneath it stays open for that press (DEC-6). - **FR5 — Associations are explicit.** Every active or blocking owner records an optional parent owner and an interaction-branch root. A passive dependent records its associated owner or trigger without creating an owner node or @@ -187,6 +190,8 @@ Current `main` does not satisfy this spec completely: - the stack has `close` and `block` but no passive role; Tooltip currently consumes Escape ahead of associated owners; +- no Tooltip or HoverCard pins open on a press yet (`component:Tooltip` FR9, + `component:HoverCard` FR9); - no shared owner/branch association graph or outside-branch resolver exists; - DropdownMenuSubMenu has a local parent-close chain, but outside channels do not consume shared branch identity; @@ -205,8 +210,8 @@ Current `main` does not satisfy this spec completely: and passive roles plus parent-owner and branch-root association. Add pure eligible-owner and outside-branch resolvers. Preserve the current Escape/platform contract for existing active/blocking members while migrating. -2. **Eligibility migration.** Make Tooltip passive and update Tooltip-over-owner - tests. Keep HoverCard active because it can own interactive content. Migrate +2. **Eligibility migration.** Make an unpinned Tooltip passive and a pinned + Tooltip active, and update Tooltip-over-owner tests. Keep HoverCard active because it can own interactive content. Migrate CommandPalette and the local Escape implementations tracked by `family:overlay-dismissal` onto eligible-owner delivery. 3. **Branch migration.** Project DropdownMenuContext's root/submenu chain into a @@ -233,21 +238,22 @@ Interaction modality continues to follow `architecture:interaction-modality`. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| --------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| FR1–FR6 | Pure registry/resolver tests plus family interaction tests | active, blocking, passive Tooltip, visual-only, controlled owner | Passive surface wins, blocker leaks, or wrong owner closes | -| FR7–FR10 | Branch resolver tests and real-browser outside interaction | root menu with two submenu levels; parent + child Popover; associated trigger | Outside root leaves submenu open, or child-only outside closes parent | -| FR11–FR14 | Gesture-counter and browser event-order tests | native light dismiss before/after click; menu button; hover-open; touch; backdrop; swipe | Same gesture reopens any associated trigger or exposed hover reopens panel | -| FR15–FR20 | Chromium/WebKit native host tests | Toast at root; Toast through one/two modals; CommandPalette above modal; ordinary local Popover | Global surface is behind/inert, loses state, or ordinary overlay becomes global | -| FR21–FR23 | Supported/unsupported browser matrix and public-doc checks | anchor/fixed/custom; auto/manual; explicit controls; absent feature APIs | Docs promise equivalence or reduced fallback is unusable | -| IR1–IR3 | API/type tests and mutation tests for resolvers/claims | one-at-a-time channel versus branch channel | One topmost query is reused for outside branches or timestamps replace identity | -| IR4, IR5 | Public-surface snapshot and atomic change review | local Popover, global Toast, global CommandPalette | Generic global prop appears or unrelated behaviors are coupled in one change | +| Contract | Verification | Representative states | Mutation or failure expectation | +| --------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| FR1–FR6 | Pure registry/resolver tests plus family interaction tests | active, blocking, passive Tooltip, pinned Tooltip and HoverCard over a Dialog, visual-only, controlled owner | Passive surface wins, a pinned surface lets Escape reach the owner beneath, blocker leaks, or wrong owner closes | +| FR7–FR10 | Branch resolver tests and real-browser outside interaction | root menu with two submenu levels; parent + child Popover; associated trigger | Outside root leaves submenu open, or child-only outside closes parent | +| FR11–FR14 | Gesture-counter and browser event-order tests | native light dismiss before/after click; menu button; hover-open; touch; backdrop; swipe | Same gesture reopens any associated trigger or exposed hover reopens panel | +| FR15–FR20 | Chromium/WebKit native host tests | Toast at root; Toast through one/two modals; CommandPalette above modal; ordinary local Popover | Global surface is behind/inert, loses state, or ordinary overlay becomes global | +| FR21–FR23 | Supported/unsupported browser matrix and public-doc checks | anchor/fixed/custom; auto/manual; explicit controls; absent feature APIs | Docs promise equivalence or reduced fallback is unusable | +| IR1–IR3 | API/type tests and mutation tests for resolvers/claims | one-at-a-time channel versus branch channel | One topmost query is reused for outside branches or timestamps replace identity | +| IR4, IR5 | Public-surface snapshot and atomic change review | local Popover, global Toast, global CommandPalette | Generic global prop appears or unrelated behaviors are coupled in one change | ### Completion criteria This spec is complete only when: -- Tooltip is passive in Escape/platform and outside-branch coordination; +- an unpinned Tooltip is passive in Escape/platform and outside-branch + coordination, and a pinned Tooltip or HoverCard takes Escape first; - eligible-owner and associated-branch operations are shared and adopted by all members named by `family:overlay-dismissal` for their relevant channels; - outside a dropdown/menu root closes its entire open submenu cascade, while @@ -273,8 +279,9 @@ This spec is complete only when: **Decider:** `cixzhang`, `2026-08-30` Escape and platform close select the topmost eligible active or blocking owner, -not the topmost visible or registered surface. Tooltip is passive and cannot -shield a Popover, menu, Dialog, or associated interactive element beneath it. +not the topmost visible or registered surface. An unpinned Tooltip is passive +and cannot shield a Popover, menu, Dialog, or associated interactive element +beneath it; a pinned one is the person's own interaction (DEC-6). Rejected: letting every visible transient layer consume one Escape. That makes supplementary information block the interaction the user is trying to dismiss. @@ -329,6 +336,18 @@ not equivalent positioning, stacking, association, or dismissal behavior. Rejected: describing the current `display` fallback as a polyfill. It does not implement the missing browser systems. +### DEC-6 — A pinned layer is the person's own interaction + +**Reference:** `spec:AST-003/DEC-6` +**Decider:** `cixzhang`, `2026-10-31` + +A Tooltip or HoverCard that a press pinned open is something the person asked +to keep, so it is an active owner and Escape closes it before the owner beneath +it. A Tooltip that only previews on hover or focus stays passive. + +Rejected: keeping every Tooltip passive, which lets Escape close a Dialog behind +a tip the person pinned. + ## Open questions None. diff --git a/docs/specs/AST-082-native-first-surfaces/spec.md b/docs/specs/AST-082-native-first-surfaces/spec.md index 7830f8b39a9bc..cda613d027ea1 100644 --- a/docs/specs/AST-082-native-first-surfaces/spec.md +++ b/docs/specs/AST-082-native-first-surfaces/spec.md @@ -85,7 +85,7 @@ depends only on runtimes it can maintain. announcements). It is the same on every platform that offers the component. - A platform honors a prop when the prop behaves the way core already defines for that platform's input type. `tooltip` stays in the core: core defines - what a tooltip does where there is no hover (Tooltip `touchTrigger`), and a + what a tooltip does where there is no hover (Tooltip `pressTrigger`), and a touch platform behaves that way. - A prop leaves the core only when a platform that offers the component cannot match core's defined behavior for its input type. Such a prop belongs to the diff --git a/docs/specs/AST-090-exit-motion/spec.md b/docs/specs/AST-090-exit-motion/spec.md index cdda259f2dec2..613aa362bae4b 100644 --- a/docs/specs/AST-090-exit-motion/spec.md +++ b/docs/specs/AST-090-exit-motion/spec.md @@ -124,13 +124,19 @@ governs them in a touch presentation. - **FR4 — A leaving surface takes no input.** From the moment its close begins, no pointer press or key activates anything in a leaving surface's own content. Toasts that a leaving dialog hosts are not its content (FR13). -- **FR5 — A leaving surface keeps its dismissal role.** Until it is hidden, a - leaving surface keeps the dismissal role it had while open (`spec:AST-003` - FR1). A leaving active or blocking owner stays present on the shared +- **FR5 — A leaving surface keeps its dismissal role, apart from a closing + pinned layer.** Until it is hidden, a leaving surface keeps the dismissal role + it had while open (`spec:AST-003` FR1), except a closing pinned `Tooltip` or + `HoverCard`, below. A leaving active or blocking owner stays present on the shared dismissal stack (`family:overlay-dismissal` FR2), so an Escape press or - platform close request during its exit does not reach an owner beneath it. A - leaving passive dependent, such as `Tooltip`, stays passive and shields - nothing (`spec:AST-003` FR4). + platform close request during its exit does not reach an owner beneath it, + except a closing pinned `Tooltip` or `HoverCard`, which leaves the stack when + its exit starts. A + leaving passive dependent, such as an unpinned `Tooltip`, stays passive and + shields nothing (`spec:AST-003` FR4). A pinned `Tooltip` or `HoverCard` that + is closing plays its exit and stops taking Escape once the exit starts: it is + no longer the stack's owner, so the member beneath it takes the next Escape + (`spec:AST-003` FR4, DEC-6). - **FR6 — Focus leaves with the close.** A layer surface hands focus back when its close begins, as it does with the exit off, and from then on its exit is hidden from assistive technology: a screen reader neither reads nor announces @@ -199,17 +205,22 @@ governs them in a touch presentation. it does with the exit off, and its exit plays after that on the runtime's shared exit copy (INV14), drawn in the top layer where the layer was. Native close reconciliation stays single-fired (INV6). A leaving active or blocking - owner keeps its place on the dismissal stack until its exit ends (FR5). A + owner keeps its place on the dismissal stack until its exit ends, except a + closing pinned Tooltip or HoverCard, which leaves it when its exit starts + (FR5). A leaving Dialog keeps supplying its modal outlet until it is hidden (FR13); BottomSheet's outlet behavior does not change. - `architecture:component-style-authoring`: no claim changes. A layer's exit is the one transient exit copy INV7 allows, made by the shared mechanism and removed when the exit ends (FR9). - `family:overlay-dismissal`: no claim changes. A leaving member stays present - for FR2 until it is hidden, and animation stays member-owned (AV6). + for FR2 until it is hidden, except a closing pinned Tooltip or HoverCard, + which leaves the stack when its exit starts (FR5), and animation stays + member-owned (AV6). - `spec:AST-003`: no claim changes. A leaving surface keeps its interaction role - (FR1), and a leaving passive dependent such as `Tooltip` still never shields - an owner (FR4, DEC-1). A leaving dialog is still the current native modal, so + (FR1), a leaving passive dependent such as an unpinned `Tooltip` still never + shields an owner (FR4, DEC-1), and a closing pinned `Tooltip` or `HoverCard` + gives up Escape when its exit starts (FR4, DEC-6). A leaving dialog is still the current native modal, so it keeps its outlet and toasts stay operable above it (FR16, FR18). - `spec:AST-094`: no claim changes. A touch presentation keeps the exit FR7 and FR8 give it (FR14), and its motion roles stay internal (FR4, DEC-15). The exit @@ -242,23 +253,23 @@ governs them in a touch presentation. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| FR1 | Component unit suites run unchanged with default tokens | Every dialog and layer member in a pointer presentation, opened and closed | A surface stays rendered after its close with default tokens | -| FR2 | Real-browser test under a theme that sets one kind, fine primary pointer | Dialogs on with layers off; layers on with dialogs off; a nested `Theme` | The unset kind animates, or the set kind closes at once | -| FR3 | Real-browser frame sampling during an exit | A dialog with and without an opener, a fullscreen dialog, a layer in each placement, right-to-left | The surface leaves its entrance path, or keeps the entrance curve | -| FR4 | Real-browser test | A press and an Enter key on a control inside a leaving surface | The control activates | -| FR5 | Dismissal invariant tests | Escape during a leaving Dialog or Popover with an owner beneath; Escape during a leaving Tooltip over a Dialog | The owner beneath a leaving owner closes, or a leaving Tooltip absorbs the press | -| FR6 | Unit and real-browser focus and accessibility-tree tests | Dialog and layer closes, by keyboard and by pointer; the accessibility tree during a layer exit | Dialog focus returns before the dialog is hidden or lands on the body; layer focus waits for the exit; a leaving layer's exit is in the accessibility tree | -| FR7 | Unit tests | Controlled and uncontrolled closes | A callback or `isOpen` waits for the exit | -| FR8 | Unit and real-browser tests | Reopen during an exit, unmount during an exit, `Activity` hidden during an exit | The surface disappears and reappears, or stays after unmount | -| FR9 | Unit test with a suppressed end event; real-browser test with animations and transitions disabled page-wide | Open and close in one update; page-wide `transition: none`; no end event | The surface is present more than 100ms after its exit duration | -| FR10 | Real-browser test with reduced motion emulated, fine primary pointer | Each kind turned on | An exit plays | -| FR11 | Real-browser frame sampling during an exit | A hover card, a menu with an open sub-menu, a selector with a search query | The leaving surface shows other content, or none, before it is hidden | -| FR12 | Token default tests, theme template inventory test, token documentation check | Defaults; the `BottomSheet` closing curve | A default drifts, or `BottomSheet` restates the curve | -| FR13 | Real-browser test | A toast action pressed, and a toast dismissed, during a dialog exit; the same toast after the dialog is hidden | The toast ignores input or fades with the dialog during the exit, or loses its entries or timers when the dialog is hidden | -| FR14 | Real-browser test with a coarse primary pointer, after hydration | A Dialog, a Popover, and a menu opened and closed by a finger, with each kind switched off and on; the same surfaces under reduced motion | A switch changes the touch exit, or the surface closes without the exit `spec:AST-094` FR7 and FR8 give it | -| Platform | Real-browser tests in Chromium and WebKit | A dialog exit and a layer exit with each kind on; a layer close where the Popover API is missing | A layer exit paints outside the top layer or does not play, or a close without the Popover API is delayed | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1 | Component unit suites run unchanged with default tokens | Every dialog and layer member in a pointer presentation, opened and closed | A surface stays rendered after its close with default tokens | +| FR2 | Real-browser test under a theme that sets one kind, fine primary pointer | Dialogs on with layers off; layers on with dialogs off; a nested `Theme` | The unset kind animates, or the set kind closes at once | +| FR3 | Real-browser frame sampling during an exit | A dialog with and without an opener, a fullscreen dialog, a layer in each placement, right-to-left | The surface leaves its entrance path, or keeps the entrance curve | +| FR4 | Real-browser test | A press and an Enter key on a control inside a leaving surface | The control activates | +| FR5 | Dismissal invariant tests | Escape during a leaving Dialog or Popover with an owner beneath; Escape during a leaving unpinned Tooltip over a Dialog; Escape during the exit of a pinned Tooltip or HoverCard over a Dialog | The owner beneath a leaving owner closes, a leaving unpinned Tooltip absorbs the press, or a closing pinned Tooltip or HoverCard takes an Escape after its exit starts | +| FR6 | Unit and real-browser focus and accessibility-tree tests | Dialog and layer closes, by keyboard and by pointer; the accessibility tree during a layer exit | Dialog focus returns before the dialog is hidden or lands on the body; layer focus waits for the exit; a leaving layer's exit is in the accessibility tree | +| FR7 | Unit tests | Controlled and uncontrolled closes | A callback or `isOpen` waits for the exit | +| FR8 | Unit and real-browser tests | Reopen during an exit, unmount during an exit, `Activity` hidden during an exit | The surface disappears and reappears, or stays after unmount | +| FR9 | Unit test with a suppressed end event; real-browser test with animations and transitions disabled page-wide | Open and close in one update; page-wide `transition: none`; no end event | The surface is present more than 100ms after its exit duration | +| FR10 | Real-browser test with reduced motion emulated, fine primary pointer | Each kind turned on | An exit plays | +| FR11 | Real-browser frame sampling during an exit | A hover card, a menu with an open sub-menu, a selector with a search query | The leaving surface shows other content, or none, before it is hidden | +| FR12 | Token default tests, theme template inventory test, token documentation check | Defaults; the `BottomSheet` closing curve | A default drifts, or `BottomSheet` restates the curve | +| FR13 | Real-browser test | A toast action pressed, and a toast dismissed, during a dialog exit; the same toast after the dialog is hidden | The toast ignores input or fades with the dialog during the exit, or loses its entries or timers when the dialog is hidden | +| FR14 | Real-browser test with a coarse primary pointer, after hydration | A Dialog, a Popover, and a menu opened and closed by a finger, with each kind switched off and on; the same surfaces under reduced motion | A switch changes the touch exit, or the surface closes without the exit `spec:AST-094` FR7 and FR8 give it | +| Platform | Real-browser tests in Chromium and WebKit | A dialog exit and a layer exit with each kind on; a layer close where the Popover API is missing | A layer exit paints outside the top layer or does not play, or a close without the Popover API is delayed | ## Decision log diff --git a/packages/core/src/Field/FieldLabel.spec.md b/packages/core/src/Field/FieldLabel.spec.md index ecc40a708b6de..13769a5e57ca9 100644 --- a/packages/core/src/Field/FieldLabel.spec.md +++ b/packages/core/src/Field/FieldLabel.spec.md @@ -38,15 +38,15 @@ system_specs: [spec:AST-020/FR1] ## Contract at a glance -| Area | Contract | -| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Public contract | `FieldLabel` renders a visible or visually hidden label group for one control or a caller-labelled group, with optional description, optionality indicator, icon, tooltip, label-element BaseProps, and label-element ref. | -| Behavior | Single-control mode uses a native label; group mode uses a labelled span. A description adds a wrapper div, while caller props and ref stay on the inner label element. Description clicks forward only to one associated control. | -| End-user impact | People can identify a control or group, hear supporting text separately from its name, and activate eligible controls from the complete visible label area; a tap on the tooltip's info icon opens or closes the tooltip instead (DEC-1). The current tooltip remains unavailable to keyboard and screen-reader users. | -| Builder impact | Builders provide stable control and description IDs, select group mode when labelling a composite, and wire `aria-labelledby` or `aria-describedby` on the controlled surface. | -| Compatibility/readiness | Records shipped behavior and DEC-1; no API, default, or DOM change. Group mode renders a span while public types promise label props and ref, and the tooltip icon exposes its content only to a pointer; both are listed under Current-state impact. | -| Review checks | Reject a lost native/group association, nested description in the accessible name, forwarded clicks from group or interactive description content, dropped label-element props/ref, hidden tooltip content, or undocumented public prop. | -| Governing rules | Established public interface; HTML label semantics; WCAG 2.2 SC 2.1.1, SC 4.1.2; `architecture:public-component-api/INV1,INV5–INV9,INV12`; `architecture:component-theming-surface/INV3–INV7`; `spec:AST-020/FR1`. | +| Area | Contract | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Public contract | `FieldLabel` renders a visible or visually hidden label group for one control or a caller-labelled group, with optional description, optionality indicator, icon, tooltip, label-element BaseProps, and label-element ref. | +| Behavior | Single-control mode uses a native label; group mode uses a labelled span. A description adds a wrapper div, while caller props and ref stay on the inner label element. Description clicks forward only to one associated control. | +| End-user impact | People can identify a control or group, hear supporting text separately from its name, and activate eligible controls from the complete visible label area. Help behind the info button opens by pointer, touch, keyboard, and screen reader, and a tap, click, or key press on it never changes the control (DEC-1); the shipped icon stays pointer-only, and a mouse click on it still activates the control, until FR9 is built. | +| Builder impact | Builders provide stable control and description IDs, select group mode when labelling a composite, and wire `aria-labelledby` or `aria-describedby` on the controlled surface. | +| Compatibility/readiness | No API or default change; with `labelTooltip`, the info trigger becomes an InfoTip button after the label element, and a mouse click on it stops activating the control (FR9, DEC-1; not yet built). Group mode renders a span while public types promise label props and ref, and the shipped tooltip icon exposes its content only to a pointer; both are listed under Current-state impact. | +| Review checks | Reject a lost native/group association, nested description in the accessible name, forwarded clicks from group or interactive description content, dropped label-element props/ref, hidden tooltip content, an info trigger inside the label element or one whose press activates the control, or undocumented public prop. | +| Governing rules | Established public interface; HTML label semantics; WCAG 2.2 SC 2.1.1, SC 4.1.2; `component:InfoTip`; `architecture:public-component-api/INV1,INV5–INV9,INV12`; `architecture:component-theming-surface/INV3–INV7`; `spec:AST-020/FR1`. | This table is a review projection; the body below is authoritative. @@ -57,7 +57,7 @@ This table is a review projection; the body below is authoritative. ## Compatibility and migration - Released default preserved: yes. -- Compatibility class: records shipped behavior and DEC-1; no public prop, default, export, or DOM change. +- Compatibility class: no public prop, default, or export change. With `labelTooltip`, the info trigger moves from an icon inside the label element to a `component:InfoTip` button after it, in one inline-flex row wrapping both, and a press on it, by any pointer or key, does not activate the control (FR9, DEC-1). Callers see that extra wrapper only when `labelTooltip` is set; their class, `xstyle`, and ref stay on the label element (FR6). - Controlled/uncontrolled behavior: not applicable. - Migration decision: DEC-1 needs no caller change; the ref typing is OQ1. @@ -76,33 +76,33 @@ This table is a review projection; the body below is authoritative. - The associated control or group's value, disabled semantics, focus model, keyboard behavior, and ARIA wiring beyond the label's own IDs. - The caller's layout around the label group. -- Tooltip surface behavior and icon rendering, which delegate to Tooltip and `component:Icon`; FieldLabel still owns whether its chosen trigger exposes that content. +- The info trigger, its press toggle, and the tooltip surface, which delegate to `component:InfoTip` and through it to `component:Tooltip`; FieldLabel owns where the trigger renders, its name, and that its content is exposed. ## Public concepts -| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior | -| ---------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -------------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| label mode | single control; group | Chooses a native `