Repository navigation
feat(Layer): one input hold, first used by Dialog, Drawer and MobileNav (spec:AST-094 FR35) - #8342
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
cf96937 to
5f964ec
Compare
5f964ec to
bc6133a
Compare
There was a problem hiding this comment.
This pull request is on hold for a spec decision: the shared presentation hold in packages/core/src/Layer/presentationHold.ts and the draft spec:AST-094 requirements (FR1–FR23) have no current governing record, so an engineering owner needs to settle whether the Layer module owns one hold for every layer (today architecture:layer-runtime INV9 keeps pointer and touch handlers with their owning component), identify the record that carries it, and approve or reject AST-094 by commenting /approve-spec <full head SHA>; the author may do so as an engineering owner. Independently of that decision, these changes are needed:
- Land the six dependency pull requests and rebase, so the compare against
mainholds only the presentation-hold commit instead of 37,287 authored lines. - Release the
usePressFeedbacktext-selection lock with a non-experimental changeset, sincespec:AST-017FR22 keeps stable runtime behavior out of[experimental]. - Remove the
platformadaptation axis frompackages/cli/assets/theme.template.ts, whichdefineThemerejects. - Let the status effect in
useAlertMotionrelease its own hold, so StrictMode's effect replay acquires it again. - Correct the two changesets that misstate the focus change and the theme
mdpoint.
[Automated review]
| * are primary-pointer precision, contrast preference, motion preference, and | ||
| * platform (`ios` for the browsers of iPhone and iPad, `other` for the rest). |
There was a problem hiding this comment.
This lists a platform adaptation axis, but defineTheme refuses when: {platform: 'ios'} (packages/core/src/theme/expandMotionRoles.test.ts:137-145) and spec:AST-012 FR2 closes when to width, height, pointer, contrast and motion. A theme author who follows this gets a build error; drop the platform clause.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [experimental] A held finger or pen no longer starts text selection or the iOS callout on a press-tracked control or cell, or on a recognizer created with the new `preventsTextSelection` option (`@astryxdesign/core/gestures/experimental`), as on UIKit controls. The gesture host turns them off for the contact and restores them when it ends, on a non-text input under the finger inside such an element too (WebKit's own style makes every input selectable), and only for recognizers that would take the touch; the document press controller (`usePressFeedback` rows) takes the same lock on any coarse pointer. Text fields keep their selection, a mouse keeps desktop selection, and other pans leave their content selectable. |
There was a problem hiding this comment.
The usePressFeedback part of this changes stable runtime behavior on Button, Link, Item and every other press-feedback surface under a finger, which spec:AST-017 FR22 keeps out of [experimental]. Give that part its own [fix] or [feat] entry naming the stable change.
| // Unmounting lets the alert go at once, hold included. | ||
| useEffect( | ||
| () => () => { | ||
| releaseHoldRef.current?.(); | ||
| releaseHoldRef.current = null; | ||
| }, | ||
| [], | ||
| ); |
There was a problem hiding this comment.
Under <StrictMode> this cleanup runs in the effect replay and releases the hold that the status effect below acquired; for an alert mounted open, the replayed status effect returns at was === now, so the hold never comes back in development (architecture:react-component-runtime INV3). Letting the status effect own the release of its hold would make the replay re-acquire it.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: one presentation hold for every layer (`Layer/presentationHold`). While a layer presents, a touch, a press or a click anywhere reaches nothing (keys and assistive technology still act); while it dismisses, the same, and the layer's content is inert. The gesture host holds the layer's touches through its touch gates. AlertDialog's touch presentation now uses it, with no change in behavior. Not exported from the package entry point. |
There was a problem hiding this comment.
Focus now leaves the fading alert's content (its children become inert), as the pull request description says, so “no change in behavior” undersells it; name the focus change.
| // and large touch displays stay anchored, phones in either orientation get | ||
| // the sheet. The query is the shared touch presentation's | ||
| // (utils/touchPresentation.ts). | ||
| export const COMPACT_TOUCH_PRESENTATION_QUERY = COMPACT_TOUCH_QUERY; |
There was a problem hiding this comment.
This is the default compact definition, so a theme that overrides md still gets 768 px here, while .changeset/touch-presentation.md promises the theme's md point (spec:AST-012 FR9 asks for the effective md). Either read the active theme's definition or say 768 px in the note.
bc6133a to
77d538e
Compare
Under a finger AlertDialog held its own input during its presentation: the action tracker dropped touches through an isInert predicate, and render guards dropped a click, Enter or Escape while it left. It now uses the Layer module's one hold (meta#8342): - useAlertMotion holds the alert with scope 'viewport' from the presentation's start until UIKit's touch window after the start the browser reports (alertController.present.ignoresTouchesForMs, a cancellable timer), or until the reduced-motion cross-fade ends; and with scope 'layer' until the dismissal's fade ends. acceptsTouches reads the hold. - AlertDialogTouch drops the tracker's isInert predicate: the gesture host's touch gate holds the alert's touches. AlertDialog drops its click guards; the Escape guard stays, since inert content does not stop the dialog's own cancel event. One detail moves: while the alert fades out, focus leaves its inert content, and Dialog still returns it to the opener at the end. The leave test now also checks what a tap hits (the shield, on the alert and over the opener) and a key during the exit (focus already out, nothing runs).
d7f968b to
2eec9ff
Compare
There was a problem hiding this comment.
This pull request is on hold for a spec decision. No current record states a shared presentation hold in the Layer module (packages/core/src/Layer/presentationHold.ts), and architecture:layer-runtime INV9 keeps backdrop, pointer and touch handlers with their owning component. An engineering owner needs to provide or identify the record that states the hold and its shield, and to record that decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner. Whatever that decision is, these changes are needed:
- Land #8045, #8062, #8074, #8075 and #8336 and rebase, so that the compare against
mainholds only the presentation-hold commits instead of 32,819 authored lines. - Give the velocity tracker the stillness reset that
spec:AST-100FR2 requires, and drop its alternative filters and thevelocityStrategyoption (architecture:interaction-modalityINV10);PanGestureRecognizer.test.tscurrently expects 2,000 px/s after a still pause. - File
.changeset/reduced-motion-record.mdas an internal change, since the animator it describes is not exported. - Either limit the press-feedback selection lock to touch, or show the before-and-after pen evidence that
spec:AST-017FR59 asks for.
[Automated review]
| * header describes, `uikit` by default. | ||
| * @experimental | ||
| */ | ||
| export type VelocityStrategy = 'uikit' | 'lsq2' | 'iosFling' | 'window'; |
There was a problem hiding this comment.
spec:AST-100 (current) says the first Core velocity source has "no selectable alternative filters", and architecture:interaction-modality INV10 keeps one release filter. FR2 also requires a release of 0 after a still finger, while the uikit filter "never decays" (lines 13-20) and PanGestureRecognizer.test.ts:77-92 expects 2,000 px/s after a 100 ms rest. Keeping only uikit, adding the stillness reset and removing velocityStrategy would conform.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] The experimental touch animator follows design:reduced-motion. Under Reduce Motion every `animate` and `play` (travel or opacity, on a spring or any curve) becomes an opacity-only cross-fade of 200 ms on a coarse primary pointer, or for a call marked `touchRelease`, and completes at once on any other pointer; nothing moves, scales or springs, and travel lands with a plain style write while the element is transparent, never as an animated transform. `timing.reducedMotionCrossFadeMs` is now 200 ms and is no longer a theme role. The `reducedMotion` and `crossfade` call options are removed; `crossfadeAs` stays. `animator.reducedMotion()` reports the mode for feedback with no fade of its own, `reducedMotionFade()` gives the same fade to code beside the animator, and `reducedMotion.stylex.ts` holds its constants for StyleX transitions. |
There was a problem hiding this comment.
The animator has no export path at this head, and uikit-motion-animator-curves.md already records this behavior as [chore] Internal. As a [fix] naming removed reducedMotion and crossfade options, this reads as a change to an API consumers could import. Filing it as [chore] Internal would match its siblings.
| if (!isHeld || layer == null) { | ||
| return; | ||
| } | ||
| // Released by the cleanup alone. |
There was a problem hiding this comment.
architecture:react-component-runtime INV3 asks a new Effect to name the external system it synchronizes. A comment saying it synchronizes the layer's hold (its shield, its content's inert, the gesture host's gate) with isHeld would cover it; the acquire and release pairing is already right.
| @@ -283,6 +301,9 @@ function onPointerDown(event: PointerEvent): void { | |||
| gesture: idlePressGesture(), | |||
| pointerId: event.pointerId, | |||
| timer: null, | |||
| // A held finger never starts text selection on the surface (UIKit's | |||
| // controls and cells never do), on any coarse pointer. | |||
| unlock: lockSelection([element], event.target), | |||
There was a problem hiding this comment.
This lock also runs for a pen (only mouse returns early at line 270), and a pen is a fine pointer under spec:AST-017 FR59, which asks for before-and-after fine-pointer evidence. Either limit the lock to pointerType === 'touch', or keep pen and show that evidence.
There was a problem hiding this comment.
Approval waits on an engineering owner's spec decision: no current record states the input hold's dismissal scope (scope: 'layer', which nothing calls yet), and the stacked commits add new requirements to spec:AST-094 FR11 and FR19, architecture:react-component-runtime INV13, component:Drawer FR5, component:BottomSheetSwitcher FR5 and component:MobileNav FR3. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner); otherwise the scope can be dropped and the record edits can land through #8360 and #8098 under their own approval. Whatever is decided, these still need changes:
- Land the four stacked pull requests and rebase, so this compare is the input-hold commit alone (24,463 authored lines today, 19,753 of them stacked).
- Have MobileNav call
holdEntranceInputinstead of its own copy, and route the hold's window listeners through the shared input stream. - Keep a resting mouse's first press during an entrance on a phone or tablet, which
spec:AST-094FR35 requires ("A mouse keeps today's behavior"). - Correct the changesets and docs that say "On a touch screen" and promise that a mouse "still clicks", and the Chinese MobileNav text that says a close holds input.
[Automated review]
| // entrance only. Under a mouse nothing is held. The pointer is the one | ||
| // condition this presentation names (AST-094 FR13), read as the drawer | ||
| // opens. | ||
| const isTouch = |
There was a problem hiding this comment.
This repeats what holdEntranceInput (Layer/holdEntranceInput.ts:51-72) does for Dialog and Drawer: a coarse-pointer read here and slideInEnd (lines 383-401) waiting on the drawer's animations, then holdInput at 673-678. The two already differ (the helper keeps only running, finite animations and has no cap). Let the helper take the element whose entrance it reads and an optional cap, and call it once here.
| } | ||
| } | ||
|
|
||
| for (const type of POINTER_EVENTS) { |
There was a problem hiding this comment.
These 14 window capture listeners sit beside utils/inputStream.ts, which this compare makes the one entry for document-wide input listeners. Press feedback subscribes on every pressable's mount, so the stream's listeners are registered first and hear a held press before this swallows it, which contradicts the header at lines 53-55. Subscribing through the stream (first position, able to end the dispatch) would also give the hold a pointer it can read before a hold starts.
| state.shield = addShield(layer, state.isShieldFull); | ||
| // A presentation held while a mouse is the latest pointer heard lets it | ||
| // through from the start. | ||
| if ( |
There was a problem hiding this comment.
latestPointer is cleared with the document's last hold (lines 268-269), so on a phone or tablet with a mouse every entrance starts with the shield taking hits, and a press where the mouse already rests meets it (DialogTouchEntrance.interaction.browser.spec.ts:394-402). spec:AST-094 FR35 keeps a mouse's behavior unchanged, and its verification row fails a changed mouse path.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] On a touch screen, a tap or a drag that lands while a `Dialog` or a `Drawer` is still entering no longer closes it, runs one of its actions, moves focus or scrolls the page: it reaches nothing until the entrance has played, and the dialog or drawer finishes entering and stays open. Around a `Drawer` without a scrim only a tap or a drag on the panel is held, and the page stays live. Keys and assistive technology stay live during the entrance, the content stays in the accessibility tree, a pinch still zooms the page, and a mouse or a trackpad still clicks. |
There was a problem hiding this comment.
"On a touch screen" also covers a touchscreen laptop, whose finger is not held (DialogTouchEntrance.test.tsx:135), and "a mouse or a trackpad still clicks" does not hold for a press before the mouse moves on a coarse-primary device. Suggest "On a phone or tablet (a coarse primary pointer)" and "still clicks once it has moved"; the same wording is in Dialog.doc.mjs:72 and Drawer.doc.mjs:175.
| type: '(isOpen: boolean) => void', | ||
| description: | ||
| '当抽屉可见性变化时调用(点击背景遮罩、按 Escape 键或点击关闭按钮)。', | ||
| '当抽屉可见性变化时调用(点击背景遮罩、按 Escape 键或点击关闭按钮)。使用手指操作时,抽屉滑入期间不接受输入;关闭后、对话框关闭之前也不接受输入,随后焦点回到打开它的元素。', |
There was a problem hiding this comment.
This Chinese text says the drawer also takes no input after a close until the dialog closes, but a close holds nothing at this head (MobileNav.tsx:643-645, MobileNavTouchPresentation.test.tsx:185). Translating the English description would fix it.
There was a problem hiding this comment.
Approval waits on an engineering owner's spec decision: no current record states the input hold's dismissal scope (scope: 'layer', which nothing calls yet), and the stacked commits write new requirements into spec:AST-094 FR11 and FR19, architecture:react-component-runtime INV13, component:Drawer FR5, component:BottomSheetSwitcher FR5 and component:MobileNav FR3. An engineering owner records that decision by commenting /approve-spec with the full head SHA (the author may, as an engineering owner), or the scope is dropped and the record edits land through their own pull requests. Whatever is decided, these still need changes:
- Land #8360, #8098, #8309 and #8763 first, so this compare is the input hold's own 33 files rather than 24,544 lines.
- Keep a resting mouse's first press during an entrance on a phone or tablet, as
spec:AST-094FR35 requires (inputHold.ts:372-383). - Have MobileNav call
holdEntranceInput, route the hold's window listening through the shared input stream, and usecontainsAlongFlatTreeinstead of the hold's two copies. - Correct the changesets and docs that say "On a touch screen", promise a mouse still clicks, or describe a hold on MobileNav's close.
[Automated review]
| state.shield = addShield(layer, state.isShieldFull); | ||
| // A presentation held while a mouse is the latest pointer heard lets it | ||
| // through from the start. | ||
| if ( |
There was a problem hiding this comment.
latestPointer is dropped with the document's last hold (lines 268-269), so on a phone or tablet with a mouse every entrance starts with the shield taking hits, and a press where the mouse already rests lands on it (DialogTouchEntrance.interaction.browser.spec.ts:394-402 asserts this). spec:AST-094 FR35 says "A mouse keeps today's behavior". Start the shield lowered from a pointer source that already runs (the input stream's pointer channels), or have FR35 amended.
There was a problem hiding this comment.
Same fix as on the later review, at 0ac5f7a: the shield starts lowered when the stream last heard a mouse.
| } | ||
| } | ||
|
|
||
| for (const type of POINTER_EVENTS) { |
There was a problem hiding this comment.
These 14 window capture listeners sit beside utils/inputStream.ts, which this compare makes the one entry for document-wide input listeners. On a page with a pressable, the stream's listeners were registered first, so press feedback and the other subscribers hear a held press before this swallows it (a probe on the head confirms the order). Extending the stream with a first-position subscriber that can end the dispatch would keep one listener path.
There was a problem hiding this comment.
Same fix as on the later review, at 0ac5f7a: the hold is a first subscriber of the shared stream and ends the dispatch for a held event.
| } | ||
|
|
||
| /** Whether `target` is `layer` or inside it, in the flat tree. */ | ||
| function isWithin(layer: Element, target: EventTarget | null): boolean { |
There was a problem hiding this comment.
This and isInFlatTree (line 809) re-implement containsAlongFlatTree from utils/flatTree.ts, which the module already imports from. Import it and delete both copies, so a fix to the shared walk reaches the 'surface' hit check and the dismissal's focus check.
There was a problem hiding this comment.
Same fix as on the later review, at 0ac5f7a: containsAlongFlatTree replaces both copies.
| * open, so such a timer ends the hold before the slide-in does, and under | ||
| * load it can also run after it. | ||
| */ | ||
| async function slideInEnd(drawer: HTMLElement | null): Promise<unknown> { |
There was a problem hiding this comment.
This and the coarse-pointer read at line 648 repeat what holdEntranceInput (Layer/holdEntranceInput.ts:51-72) does for Dialog and Drawer, with a 550 ms cap the helper lacks. Let the helper take the element whose entrance it reads and an optional cap, and call it once here.
There was a problem hiding this comment.
Same fix as on the later review, at 0ac5f7a: MobileNav calls holdEntranceInput with its entrance element and cap.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] On a touch screen, a tap or a drag that lands while a `Dialog` or a `Drawer` is still entering no longer closes it, runs one of its actions, moves focus or scrolls the page: it reaches nothing until the entrance has played, and the dialog or drawer finishes entering and stays open. Around a `Drawer` without a scrim only a tap or a drag on the panel is held, and the page stays live. Keys and assistive technology stay live during the entrance, the content stays in the accessibility tree, a pinch still zooms the page, and a mouse or a trackpad still clicks. |
There was a problem hiding this comment.
"On a touch screen" also covers a touchscreen laptop, whose finger is not held (DialogTouchEntrance.test.tsx:135), and a resting mouse's first press is held on a phone or tablet. Say "On a phone or tablet" and "a mouse or a trackpad still clicks once it has moved".
There was a problem hiding this comment.
Same fix as on the later review, at 0ac5f7a.
| type: '(isOpen: boolean) => void', | ||
| description: | ||
| '当抽屉可见性变化时调用(点击背景遮罩、按 Escape 键或点击关闭按钮)。', | ||
| '当抽屉可见性变化时调用(点击背景遮罩、按 Escape 键或点击关闭按钮)。使用手指操作时,抽屉滑入期间不接受输入;关闭后、对话框关闭之前也不接受输入,随后焦点回到打开它的元素。', |
There was a problem hiding this comment.
This Chinese text says the drawer also takes no input after a close until the dialog closes, but a close holds nothing at this head (MobileNav.tsx:679-687). Translate the English description instead.
There was a problem hiding this comment.
Same fix as on the later review, at 0ac5f7a.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on new record text: the added component:MobileNav FR5 sentence about the slide-out after the dialog closes, and the stacked #8098 edits to component:Drawer FR5, component:BottomSheetSwitcher FR5 and component:MobileNav FR3. An engineering owner can accept them by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or #8098's text can land through #8098 first. Whatever that decision, these need changes:
- Land #8098, #8309 and #8763 first and split the two adopters, so the compare is no longer 7,671 lines.
- Keep a resting mouse's press during an entrance on a coarse-primary device (
inputHold.ts:372-383), as FR35's "A mouse keeps today's behavior" requires. - Have MobileNav call
holdEntranceInput, route the hold's window listening throughutils/inputStream.ts, and usecontainsAlongFlatTreeinstead of the two private flat-tree walks. - Correct the "On a touch screen … a mouse or a trackpad still clicks" wording in the changesets and docs, the Chinese MobileNav
onOpenChangetext, and the Drawer gap heading.
[Automated review]
| if (state.shield == null) { | ||
| state.isShieldFull = isFull || state.viewport + state.layer > 0; | ||
| state.shield = addShield(layer, state.isShieldFull); | ||
| // A presentation held while a mouse is the latest pointer heard lets it | ||
| // through from the start. | ||
| if ( |
There was a problem hiding this comment.
No pointer is known when a hold starts, and latestPointer is dropped with the document's last hold (lines 268-269), so on a phone or tablet with a mouse every entrance starts with the shield taking hits: a press where the mouse already rests reaches nothing (DialogTouchEntrance.interaction.browser.spec.ts:398-402 asserts it). spec:AST-094 FR35 keeps today's mouse behavior and its verification row fails a changed mouse path. Start the shield lowered when the last pointer heard is a mouse, from a source that already runs (the shared input stream), or have FR35 record the limit.
There was a problem hiding this comment.
Fixed at 0ac5f7a: a presentation's shield starts lowered when the shared input stream last delivered a mouse's pointer event, so a press where a resting mouse already is acts during an entrance. A finger or a pen raises it again. DialogTouchEntrance.interaction.browser.spec.ts now asserts that the resting mouse's press acts.
| } | ||
| } | ||
|
|
||
| for (const type of POINTER_EVENTS) { |
There was a problem hiding this comment.
These 14 window capture listeners and latestPointer sit beside utils/inputStream.ts, "The one entry for document-wide input listeners in core". Press feedback subscribes on every pressable's mount (usePressFeedback.ts:53), so the stream's window listeners are registered first and hand each held event to every subscriber before this one swallows it, which also makes the header's "neither the page nor the gesture host hears it" (lines 53-55) inaccurate. Subscribe through the stream with a first-position subscriber that can end the dispatch, or say why it cannot.
There was a problem hiding this comment.
Done at 0ac5f7a: the hold subscribes through utils/inputStream.ts as a first subscriber ({first: true}, new in the stream). It hears each event before every other subscriber, and its true ends the dispatch for a held event, so press feedback and the gesture host never hear it. Unit cases cover both sides and fail on the previous stream and hold.
| } | ||
|
|
||
| /** Whether `target` is `layer` or inside it, in the flat tree. */ | ||
| function isWithin(layer: Element, target: EventTarget | null): boolean { |
There was a problem hiding this comment.
This and isInFlatTree (line 809) re-implement containsAlongFlatTree from utils/flatTree.ts, which line 100 already imports from. Use it and delete both copies, keeping the instanceof Node guard at the call site, so a fix to the shared walk reaches the 'surface' hit check and the dismissal's focus check.
There was a problem hiding this comment.
Done at 0ac5f7a: both private walks are gone, and the 'surface' hit check and the dismissal's focus check use containsAlongFlatTree, keeping the instanceof Node guard at the call site.
| * open, so such a timer ends the hold before the slide-in does, and under | ||
| * load it can also run after it. | ||
| */ | ||
| async function slideInEnd(drawer: HTMLElement | null): Promise<unknown> { |
There was a problem hiding this comment.
This and the coarse-pointer read at lines 648-650 repeat what holdEntranceInput (Layer/holdEntranceInput.ts:51-72) does for Dialog and Drawer, with a 550 ms cap and no running/finite filter, so the two entrance holds already differ. Let the helper take the element whose entrance it reads and an optional cap, and call it once here.
There was a problem hiding this comment.
Done at 0ac5f7a: holdEntranceInput takes the element whose entrance it reads and an optional cap, and MobileNav calls it once ({entrance: drawer, capMs: MAX_EXIT_MS}), so the two entrance holds no longer differ.
| }, | ||
| description: | ||
| 'Modal dialog using the native <dialog> element. Modal and inline content starts with theme body text defaults. Ancestor surface/group membership ends as a whole, including group-owned state. Explicit props and unrelated contexts remain unchanged. Place intentional groups and complete required providers inside the dialog. With reduced motion the dialog never slides or scales in: on a touch screen it fades in, in 200 ms or less, and with a mouse it appears at once.', | ||
| 'Modal dialog using the native <dialog> element. Modal and inline content starts with theme body text defaults. Ancestor surface/group membership ends as a whole, including group-owned state. Explicit props and unrelated contexts remain unchanged. Place intentional groups and complete required providers inside the dialog. With reduced motion the dialog never slides or scales in: on a touch screen it fades in, in 200 ms or less, and with a mouse it appears at once. On a touch screen, a tap or a drag that lands while the dialog is still entering reaches nothing, and the dialog finishes entering; keys and assistive technology stay live, and a mouse or a trackpad still clicks.', |
There was a problem hiding this comment.
"On a touch screen" also covers a touchscreen laptop, whose finger is not held (DialogTouchEntrance.test.tsx:135-142), and "a mouse or a trackpad still clicks" misses a resting mouse's first press on a phone or tablet. Say "On a phone or tablet (a coarse primary pointer)" and "still clicks once it has moved"; the same wording is in Drawer.doc.mjs:175 and .changeset/dialog-drawer-touch-entrance-hold.md.
There was a problem hiding this comment.
Fixed at 0ac5f7a: "On a phone or tablet (a coarse primary pointer)". With the resting-mouse fix, the mouse clause now reads "a mouse or a trackpad still clicks: at once when it was the last pointer used, otherwise once it moves", here, in Drawer.doc.mjs and in the changeset.
| type: '(isOpen: boolean) => void', | ||
| description: | ||
| '当抽屉可见性变化时调用(点击背景遮罩、按 Escape 键或点击关闭按钮)。', | ||
| '当抽屉可见性变化时调用(点击背景遮罩、按 Escape 键或点击关闭按钮)。使用手指操作时,抽屉滑入期间不接受输入;关闭后、对话框关闭之前也不接受输入,随后焦点回到打开它的元素。', |
There was a problem hiding this comment.
This Chinese text says the drawer also takes no input after a close until the dialog closes, but a close holds nothing at this head (MobileNav.tsx:679-686, MobileNavTouchPresentation.test.tsx:185-206). Translate the English description instead.
There was a problem hiding this comment.
Fixed at 0ac5f7a: the Chinese text now translates the English description, and says nothing about a hold after a close.
|
/approve-spec 85da41d |
|
/approve-spec 0ac5f7a |
There was a problem hiding this comment.
Changes requested. The input hold does what spec:AST-094 FR35 asks for touch users, and the new MobileNav FR5 sentence is owner-approved at this head. Five things still need fixing:
- At 4,946 authored lines with two separable adopters folded in, land the hold with one adopter and the other after it, or, if this is an owner-directed batch, name its row in the description.
component:AlertDialogstill says alerts do not hold their entrance (AlertDialog.spec.md:87-88, plus FR7's gap note, the readiness row and FR7's verification row), although AlertDialog renders Dialog and now holds its entrance under a coarse primary pointer.- After a finger, the first press of a mouse that has not moved lands on the shield during an entrance (
inputHold.ts:290-296, asserted ininputHold.interaction.browser.spec.ts:283-309), against FR35's "A mouse keeps today's behavior", so record it as an FR35 gap in Current-state impact or conform. holdEntranceInput.ts:59-63re-implements the coarse-pointer read thatisCoarsePointerinutils/coarsePointer.tsowns, so call it instead..changeset/mobile-nav-touch-presentation.mdand MobileNav'sonOpenChangedocs say "under a finger" (and "no input") though only a coarse primary pointer is held and keys stay live, and.changeset/layer-input-hold.mdsays a press "anywhere" is held though a'surface'hold leaves the page live.
[Automated review]
| if ( | ||
| layer.ownerDocument.defaultView?.matchMedia?.(TOUCH_QUERY).matches !== true | ||
| ) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
This repeats isCoarsePointer(view) from utils/coarsePointer.ts, the shared gate for touch-only work. Calling it keeps this entrance hold in step with every other coarse-pointer gate:
if (!isCoarsePointer(layer.ownerDocument.defaultView)) {
return null;
}There was a problem hiding this comment.
Done at 7826488: it calls isCoarsePointer(layer.ownerDocument.defaultView), and the TOUCH_QUERY read and its import are gone.
| state.isShieldFull = isFull || state.viewport + state.layer > 0; | ||
| state.shield = addShield(layer, state.isShieldFull); | ||
| // A presentation lets a mouse through from the start when the stream's | ||
| // last pointer was a mouse. A press where the mouse rests then acts. | ||
| if (scope !== 'layer' && isMouseLast(layer.ownerDocument)) { | ||
| state.shield.style.pointerEvents = 'none'; | ||
| } |
There was a problem hiding this comment.
After a finger or a pen, the first press of a mouse that has not moved lands on the shield (asserted at inputHold.interaction.browser.spec.ts:283-309). spec:AST-094 FR35 says "A mouse keeps today's behavior", and its verification row fails "a changed mouse path". Record this limit as an FR35 gap in AST-094's Current-state impact, beside the touchscreen-laptop sentence, or conform.
There was a problem hiding this comment.
Recorded at 7826488 as an FR35 gap in AST-094's Current-state impact, beside the touchscreen-laptop sentence: "After a finger, the first press of a mouse that has not moved since lands on the entering presentation's shield and does nothing, where FR35 keeps today's behavior for a mouse (DEC-16); a move of the mouse lowers the shield first." The press has hit the shield before its pointer type is known, so conforming would mean synthesizing its down, up and click on the element beneath.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] Under a finger, MobileNav takes no touch until its slide-in ends. Under a mouse nothing changes. |
There was a problem hiding this comment.
The hold runs only under a coarse primary pointer (holdEntranceInput.ts:59-63), so a touchscreen laptop's finger is not held. On a phone or tablet, an unmoved mouse after a finger is held. Suggested wording, matching Dialog and Drawer: "On a phone or tablet (a coarse primary pointer), a tap or a drag during MobileNav's slide-in reaches nothing; keys and assistive technology stay live, and a mouse or a trackpad still clicks: at once when it was the last pointer used, otherwise once it moves."
There was a problem hiding this comment.
Done at 7826488: the changeset now uses your sentence as written. MobileNav's onOpenChange docs (English, dense and Chinese) and its two source comments say a coarse primary pointer, with keys and assistive technology live.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: one input hold for every layer (`Layer/inputHold`). While a layer presents, a finger's or a pen's touch, press or click anywhere reaches nothing, and a mouse passes once it moves, at once when the page last heard a mouse (keys and assistive technology still act, and a pinch still zooms the page). While it dismisses, every press and click reaches nothing, and the layer's content is inert. The gesture host asks the hold about each touch (`holdsTouch`), and `isInputShield` lets a hit test see past a hold's shield. Not exported from the package entry point; Dialog's and Drawer's entrance holds and MobileNav's drawer use it. |
There was a problem hiding this comment.
A 'surface' hold, which a Drawer without a scrim uses, holds only the layer's box and leaves the page live (inputHold.ts:460-485). Say "anywhere around a modal layer, and only on the layer around a non-modal one".
There was a problem hiding this comment.
Done at 7826488: it now says the press is held "anywhere around a modal layer, and only on the layer itself around a non-modal one, whose page stays live".
|
/approve-spec 7826488 |
There was a problem hiding this comment.
Recommending approval. The input hold does what spec:AST-094 FR35 asks for Dialog, AlertDialog, Drawer and MobileNav under a coarse primary pointer. An engineering owner approved the new record text for this head, and the size is covered as an owner-directed batch. The unit tests and the Chromium browser specs for the hold, Dialog, Drawer and MobileNav pass at this head.
Optional, non-blocking:
Dialog.tsx:646anduseDrawerDialogPresence.ts:152say "Under a finger", but the hold runs only under a coarse primary pointer; "Under a coarse primary pointer" would be accurate.- The comments at
holdEntranceInput.ts:71andinputHold.ts:349restate the code beside them and can go.
[Automated review]
| focusForLastInput(autofocusTarget); | ||
| } | ||
| } | ||
| // Under a finger, a tap or a drag during the entrance reaches nothing. |
There was a problem hiding this comment.
Non-blocking: the hold runs only under a coarse primary pointer, so a finger on a touchscreen laptop is not held. "Under a coarse primary pointer, a tap or a drag during the entrance reaches nothing." would be accurate.
| dialog.querySelector<HTMLElement>('[data-autofocus]')?.focus(); | ||
| } | ||
| return; | ||
| // Under a finger, a tap or a drag during the slide-in reaches nothing. |
There was a problem hiding this comment.
Non-blocking: same as in Dialog.tsx, the hold applies under a coarse primary pointer, not under any finger. "Under a coarse primary pointer, a tap or a drag during the slide-in reaches nothing."
| }); | ||
| } | ||
|
|
||
| /** Resolves after `ms`. */ |
There was a problem hiding this comment.
Non-blocking: this comment restates after(ms). It can go.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on new record text at this head: the added component:MobileNav FR5 sentence ("After the dialog closes, the rest of the slide-out is inert.") and the spec:AST-094 current-state note that, after a finger, an unmoved mouse's first press lands on an entering layer's shield, which departs from FR35's "A mouse keeps today's behavior". The approval given for the previous commit does not carry over to the rebased one, so an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner).
Whatever that decision, one change is needed:
packages/core/src/Layer/inputHold.ts:725-732copies the privatedeepActiveElementwalk frompackages/core/src/utils/selectionLock.ts:585-592, so export one shared helper (for example fromutils/flatTree.ts, besidecontainsAlongFlatTree) and use it in both.
Optional, non-blocking:
Dialog.tsx:646anduseDrawerDialogPresence.ts:152say "Under a finger", but the hold runs only under a coarse primary pointer, and the comments atholdEntranceInput.ts:71andinputHold.ts:349restate the code beside them.
[Automated review]
| } | ||
|
|
||
| /** The focused element, inside any shadow roots on the way down. */ | ||
| function deepActiveElement(doc: Document): Element | null { |
There was a problem hiding this comment.
This walk is the same as the private deepActiveElement in packages/core/src/utils/selectionLock.ts (lines 585-592). Export one shared helper (for example from utils/flatTree.ts, beside containsAlongFlatTree and parentAcrossShadow) and call it from both files, so a fix to the walk reaches both users.
There was a problem hiding this comment.
Done at 09ee02b: utils/flatTree.ts exports deepActiveElement beside containsAlongFlatTree and parentAcrossShadow, and both inputHold.ts and selectionLock.ts import it; flatTree.test.ts covers light DOM, a missing document, and two nested open shadow roots.
| focusForLastInput(autofocusTarget); | ||
| } | ||
| } | ||
| // Under a finger, a tap or a drag during the entrance reaches nothing. |
There was a problem hiding this comment.
Non-blocking: the hold runs only under a coarse primary pointer, so "Under a coarse primary pointer, a tap or a drag during the entrance reaches nothing." is accurate.
There was a problem hiding this comment.
Done at 09ee02b: "Under a coarse primary pointer, a tap or a drag during the entrance reaches nothing."
| dialog.querySelector<HTMLElement>('[data-autofocus]')?.focus(); | ||
| } | ||
| return; | ||
| // Under a finger, a tap or a drag during the slide-in reaches nothing. |
There was a problem hiding this comment.
Non-blocking: as in Dialog, "Under a coarse primary pointer" matches what holdEntranceInput does.
There was a problem hiding this comment.
Done at 09ee02b, with the same wording for the slide-in.
| }); | ||
| } | ||
|
|
||
| /** Resolves after `ms`. */ |
There was a problem hiding this comment.
Non-blocking: this restates the function's name and signature; it can go.
| return release; | ||
| } | ||
|
|
||
| /** The pointers whose presses are held: a finger's and a pen's. */ |
There was a problem hiding this comment.
Non-blocking: this restates the set below it; it can go.
|
/approve-spec 09ee02b |
There was a problem hiding this comment.
Recommending approval. The input hold does what spec:AST-094 FR35 asks for Dialog, AlertDialog, Drawer and MobileNav under a coarse primary pointer, the record edits carry an engineering owner's approval at this head, and sharing deepActiveElement from utils/flatTree.ts resolves the last requested change.
Nothing else is required. Optional, non-blocking:
MobileNav.tsx:524-525: the hold ref is not cleared when the hold ends on its own; keeping the release in a local of the open/close effect fixes the comment and drops three redundant releases.MobileNav.tsx:289-299andGestureHost.ts:2133:MAX_EXIT_MSalso caps the slide-in hold, andholdsTouchalso holds during a dismissal, but each comment states only the first half.
[Automated review]
| const drawerRef = useRef<HTMLDivElement>(null); | ||
| const closeTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null); | ||
| // The input hold on the drawer's layer while it slides in under a coarse | ||
| // primary pointer. It is null under a fine one and once the slide-in ends. |
There was a problem hiding this comment.
Non-blocking: nothing clears this ref when the hold ends on its own, so it is not null once the slide-in ends; the spent release stays until the effect's next run or cleanup. Keeping the release in a local of the open/close effect and releasing it in that effect's cleanup makes this comment unnecessary, along with the releases at lines 591-592, 610-611 and 650-651.
| */ | ||
| export const MAX_CLOSE_DELAY_MS = 250; | ||
| /** | ||
| * Longest the drawer slides out for, in ms. The slide-out runs for the |
There was a problem hiding this comment.
Non-blocking: lines 603-607 also use MAX_EXIT_MS to cap the slide-in hold. A name for both slides (for example MAX_SLIDE_MS), or one sentence here, makes that second use visible.
| const recognizers: GestureRecognizer[] = []; | ||
| let consumer: TouchConsumer | null = null; | ||
| let consumerElement: Element | null = null; | ||
| // A layer that the input hold holds takes no touch while it presents. |
There was a problem hiding this comment.
Non-blocking: holdsTouch also holds a layer while a 'layer' dismissal runs, so "while it presents or dismisses" is accurate.
There was a problem hiding this comment.
Approval waits on an engineering owner's spec decision at this head: this commit is the previously approved one rebased onto main with only the ci.yml path list changed, so the earlier /approve-spec no longer applies, and two pieces of record text need it again:
component:MobileNavFR5's new sentence, "After the dialog closes, the rest of the slide-out is inert.", with its verification row.spec:AST-094's current-state note that, after a finger, an unmoved mouse's first press lands on an entering presentation's shield and does nothing, which departs from FR35's "A mouse keeps today's behavior".
An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner). Nothing else blocks: the hold does what spec:AST-094 FR35 asks for Dialog, AlertDialog, Drawer and MobileNav under a coarse primary pointer, and a mid-entrance tap that closes each of them on main leaves it open here.
Optional, non-blocking:
MobileNav.tsx:524-525: the hold ref is not cleared when the hold ends on its own; keeping the release in a local of the open/close effect fixes the comment and drops three redundant releases.MobileNav.tsx:289-298andGestureHost.ts:2133:MAX_EXIT_MSalso caps the slide-in hold, andholdsTouchalso holds during a dismissal, but each comment states only the first half.
[Automated review]
|
/approve-spec c63f2f9 |
|
/approve-spec 616c6a2 |
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet. The pull request is a draft from a fork, so once it is marked ready for review, a maintainer adds the ci:run label so CI runs.
The input hold does what spec:AST-094 FR35 asks for Dialog, AlertDialog, Drawer and MobileNav under a coarse primary pointer, and the record text it adds (MobileNav FR5's inert slide-out tail and AST-094's recorded gap for an unmoved mouse after a finger) carries an engineering owner's approval for this exact head.
Optional, non-blocking:
MobileNav.tsx:524-525: the hold ref is not cleared when the hold ends on its own; keeping the release in a local of the open/close effect fixes the comment and drops three redundant releases.MobileNav.tsx:289-298andGestureHost.ts:2133:MAX_EXIT_MSalso caps the slide-in hold, andholdsTouchalso holds during a dismissal, but each comment states only the first half.
[Automated review]
|
/approve-spec 80ffcbb |
There was a problem hiding this comment.
Changes requested for one CI defect; the input hold itself and its record text are ready.
.github/scripts/ci-interaction-webkit.test.mjsfails 2 of 14 cases at this head onpackages/core/src/Layer/inputHold.ts, because the WebKit lane's path-filter line it needs moved to #9906, which is still an open draft: land #9906 first and rebase onto it, or restore those two hunks here.
Everything else matches spec:AST-094 FR35: Dialog (with AlertDialog through it), Drawer and MobileNav hold a finger's tap or drag during their entrance under a coarse primary pointer, with keys, assistive technology and pinch-zoom live, and the record edits carry an engineering owner's approval for this head. Three optional comment fixes raised earlier still apply (MobileNav.tsx:524-525 and :289-299, GestureHost.ts:2133).
[Automated review]
| import {containsAlongFlatTree} from '../utils/flatTree'; | ||
| import {isIosWebKit} from '../utils/touchQueries'; | ||
| import {type WheelSegment, WheelSegmenter} from './wheelSegmenter'; | ||
| import {holdsTouch} from '../Layer/inputHold'; |
There was a problem hiding this comment.
This import puts Layer/inputHold.ts into the gesture fixture's bundle, and .github/scripts/ci-interaction-webkit.test.mjs requires every bundled file to be in the WebKit lane's path filter. At this head the filter does not list it, so "keeps everything packages/core/src/gestures/tests/browser/fixture.ts bundles in scope" and "keeps everything the pager bundle bundles in scope" fail (:216). Land #9906 first and rebase onto it, or restore its two hunks here.
…av (spec:AST-094 FR35)
Layer/inputHold holds a layer's pointer input while its presentation
enters or leaves. holdInput(layer, {until, scope}) raises a shield:
'viewport' over the whole viewport while a modal layer enters; 'surface'
over the layer's own box while a non-modal one enters, the page around it
live; 'layer' over the viewport, its content inert, while a layer leaves.
It decides by each event's pointer (a finger's or a pen's press is held to
its end; a mouse passes once it moves with no button down, or from the
start when the page last heard a mouse, DEC-16), never cancels a touch's
start, and cancels a one-finger move so a pinch still zooms (FR10). It
hears input through Core's shared input stream before every other
subscriber, and ends the dispatch for a held event, so press feedback and the
gesture host never hear it; the stream gains first subscribers and records
the type of the latest pointer event it delivers (getLastPointerType). The gesture
host asks the hold for each touch (holdsTouch), so the hold imports nothing
from gestures/ and a component in the root entry may use it statically
(architecture:react-component-runtime INV13).
Its first users hold only under a coarse primary pointer, a touch
presentation (FR13): Dialog, Drawer and MobileNav hold a finger's tap or
drag while they enter (holdEntranceInput, which reads the entrance's own
animations; MobileNav's slide-in is capped at 550 ms), a non-modal Drawer
only on its panel. Under a fine primary pointer nothing is held and
nothing listens (FR24). Where the primary pointer is fine and the screen
also takes touch, a finger is not held yet; AST-094's current-state impact
and Drawer's record name that gap. AST-094's current-state impact also
names the first press of a still mouse after a finger, which lands on an
entering presentation's shield. AlertDialog renders Dialog, so its
entrance is held too: its record says FR7 ships in part, the leaving
alert's inert content waiting for FR6's dismissal.
Every browser proof lands its input at an exact point (an entrance held
where it is, the page's clock, or an exact step of a drag) and plays once.
The WebKit interaction lane's scope includes Layer/inputHold.ts: the
gesture host imports it, so the gesture specs' fixture bundles it.
MobileNav's record states what the rest of the slide-out after the close
does: it is inert, taking no input and hidden from assistive technology
(FR5). A browser case proves both against the open drawer.
From meta#8342 and its per-event offer, meta#8630 (Dialog and Drawer) and meta#8374
(MobileNav).
Co-authored-by: Cindy Zhang <cindyxz@meta.com>
|
/approve-spec fbe71df |
There was a problem hiding this comment.
Changes requested for one CI defect; the input hold itself and its record text, owner-approved at this head, are ready.
.github/scripts/ci-interaction-webkit.test.mjsfails 2 of 14 cases at this head onpackages/core/src/Layer/inputHold.ts, because the WebKit lane's path-filter line it needs is in #9906, which is still open: land #9906 first and rebase onto it, or restore those two hunks here.
Everything else matches spec:AST-094 FR35: Dialog (with AlertDialog through it), Drawer and MobileNav hold a finger's tap or drag during their entrance under a coarse primary pointer, with keys, assistive technology and pinch-zoom live, and AppShell is left as it is on main. CI has not run its test jobs on this head yet; a maintainer adds the ci:run label so it runs. Optional comment fixes: MobileNav.tsx:520-521, MobileNav.tsx:288-295 (MAX_EXIT_MS now only caps the slide-in hold) and GestureHost.ts:2133.
[Automated review]
| * dialog closes, while the dialog's `display` transition keeps it rendered. | ||
| * The touch hold on the slide-in ends by this time. | ||
| */ | ||
| export const MAX_EXIT_MS = 550; |
There was a problem hiding this comment.
Non-blocking: with AppShell's use gone, MAX_EXIT_MS is read only as the slide-in hold's cap (line 603), so the name says exit for an entrance cap. A name for what it bounds (for example MAX_SLIDE_MS) makes the comment's second and third sentences unnecessary.
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet, so a maintainer adds the ci:run label for it to run.
The merge of main brings the WebKit lane's path filter for packages/core/src/Layer/inputHold.ts, so .github/scripts/ci-interaction-webkit.test.mjs now passes with the hold in the gesture bundle. The input hold matches spec:AST-094 FR35 for Dialog (and AlertDialog through it), Drawer and MobileNav under a coarse primary pointer, with keys, assistive technology and pinch-zoom live, and the record text it adds is owner-approved at this head.
Optional comment fixes still stand: MobileNav.tsx:520-521 (nothing clears the hold ref when the hold ends on its own), MobileNav.tsx:288-295 (MAX_EXIT_MS now only caps the slide-in hold) and GestureHost.ts:2131 (the hold also holds during a dismissal).
[Automated review]
|
/approve-spec 72f6726 |
Batch: the maintainers' section plan, section S3, row "hold #8342+#8630+#8374": the shared input hold with its first users, folding in #8630 (Dialog and Drawer) and #8374 (MobileNav). Both close with a link here, and the commit credits their authors.
Why
On a phone, a tap or a drag can land while a Dialog, a Drawer or the MobileNav drawer is still entering, and reach whatever is under the finger: a backdrop tap closes the dialog, a button runs mid-entrance, a drag scrolls the page.
spec:AST-094FR35 asks a touch presentation to take no pointer input while it enters. Keys, assistive technology and a mouse stay live (DEC-16), and around a non-modal presentation only its own box is held.Options and tradeoffs
Why this choice
Option 3.
Layer/inputHold(internal) addsholdInput(layer, {until, scope}):'viewport'shields the viewport while a modal layer enters,'surface'only the layer's box, and'layer'the viewport while a layer leaves, its content inert. It ends whenuntilsettles, so it never times an exit.architecture:react-component-runtimeINV13).Its first users hold only under a coarse primary pointer (FR13): Dialog and Drawer while their entrance animations run, a Drawer without a scrim only on its panel, and MobileNav until its slide-in ends.
Known gaps (AST-094's Current-state impact): a finger on a touchscreen laptop isn't held, and after a finger, a still mouse's first press lands on an entering presentation's shield. AlertDialog renders Dialog, so its entrance is held too; its record says FR7 ships in part.
How to check it
In phone emulation:
Evidence
AlertDialog.spec.md's tables, where no row changed on both sides. At fbe71df:check:knowledge,check:repo, core typecheck and the touched units (2,453) pass, and so do the MobileNav accessibility spec (6) and the Chromium interaction specs (30).Activityboundary moved (308 ms to 610 ms), and the closing dialog'sdisplaytransition outranks that hide. So this pull request no longer changes AppShell.ci.ymland its routing test moved to ci: run the WebKit interaction proofs when the input hold changes #9906, which merges first (the routing test requires every file the gesture specs bundle,Layer/inputHold.tsincluded, to be in that filter). The product, specs and tests are byte-identical.@astryxdesign/a11y-spec/playwright(the product patch is byte-identical). At 616c6a2:CI=true pnpm lintreports only main's ownWheelContrast.a11y.chromium.spec.ts; build, typecheck, the Chromium interaction specs (30) and the MobileNav accessibility spec (8), WebKit in CI's container (26, 4 Chromium-only skipped), the touched units (2,453) andcheck:knowledgepass.check:repo,check:knowledge, strict ESLint and Prettier clean;pnpm test:ui19,523 passed; the node project 16,164 passed (2 expected failures);api:report --checkclean; Storybook built.deepActiveElementmoves toutils/flatTree.ts, shared by the hold and the selection lock.(pointer: coarse)false, as on a touchscreen laptop), no hold starts. Both builds give the same events (pointerdown, mousedown, pointerup, mouseup and click on the control) and the same result: the dialog and the drawer close, and MobileNav follows its link. The entrance, sampled at 12 paused times from 0 to 600 ms, differs by 0 px, with the same opacity and transform.(pointer: coarse)true), a mouse that opened the layer gets the same events, results and 0 px.Writing rubric results
Records touched:
component:Drawer(Current-state impact: the entrance-hold gap, now only the fine-primary case),spec:AST-094(path applicability for the two Layer files; Current-state impact: the fine-primary gap and the still mouse after a finger),component:Dialog(system_specsnames FR35),component:AlertDialog(Current-state impact, the readiness row, FR7's gap note and verification row, andverified_by: the entrance hold ships through Dialog), andcomponent:MobileNav(FR5: one sentence on the rest of the slide-out after the close; one verification row, itsverified_byentry andsystem_specs).