Skip to content

spec: Tooltip and HoverCard pin on press; InfoTip is the info preset - #9175

Merged
cixzhang merged 17 commits into
mainfrom
spec/press-trigger-toggle
Oct 31, 2026
Merged

cixzhang merged 17 commits into
mainfrom
spec/press-trigger-toggle

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Exact decision needed

How a person on touch, keyboard, or a screen reader keeps a tooltip open long enough to read it, and how an info icon offers that help. Today the help behind an info icon, a field label's info icon, or a status dot opens on hover, and a tap opens it when the trigger performs no action (the default touchTrigger="auto") or always with touchTrigger="tap"; but a scroll or a tap elsewhere closes it, no mouse or keyboard press keeps it open, and neither the field label's icon nor the status dot can be reached with Tab.

Existing authority searched

  • component:Tooltip, component:HoverCard: hover, focus, and tap opening (touchTrigger auto | tap | none); no press-to-pin for a mouse or keyboard.
  • spec:AST-003 FR4: every Tooltip is passive in Escape order, so a tooltip someone chose to keep open could not take Escape before a Dialog beneath it.
  • component:FieldLabel FR8, AR5: the label tooltip must be keyboard-reachable through a named trigger; the shipped icon is neither.
  • component:StatusDot FR5: an info-only trigger whose information lives only in its tooltip, with no keyboard path.
  • Lab InfoTip: a named button around Tooltip with touchTrigger="tap"; no Core record.
  • architecture:public-component-api INV9, INV21; spec:AST-017 FR28–FR29d (deprecation, codemods); spec:AST-094 FR42 (touch hit area); component:MoreMenu (a component built entirely on another).
  • Open work: fix(Tooltip, HoverCard): a disabled control's reason opens on a tap #8207's contract change (a tap that already acts stays with its control) is folded into this pull request's Tooltip and HoverCard DEC-1; its implementation lands separately. FieldLabel's FR9 and DEC-1 (a finger or pen tap on the info icon opens its help) are current; this pull request amends both in place so a tap, click, or key press on the info trigger never activates the control.
  • architecture:interaction-modality INV4: every modality has an operable path; it said nothing about a tap on an action trigger that also carries a tooltip.

No current record lets a mouse or keyboard press keep a tooltip or hover card open, or says what an info icon is.

Decision

User impact: people on a phone, a keyboard, or a screen reader can open the help behind an InfoTip, a field label's info button, or a standalone StatusDot, keep it open while they read, and close it; a StatusDot inside a button, link, menu item, or option tells screen-reader users its status through the host's accessible name; and asking what a setting does never changes the setting.

  • Tooltip and HoverCard pin on a press. A new prop, pressTrigger: 'none' | 'toggle' (default 'none'), on Tooltip, HoverCard, useTooltip, and useHoverCard. With 'toggle', hover and keyboard focus still preview the layer; a click, tap, Enter, or Space on a button trigger pins it open; Escape, a second press, a press outside, or focus moving away closes it; a pinned layer stays open when the pointer leaves. The trigger carries aria-expanded (true only while pinned) and aria-controls, and the layer is announced once through a polite live region when it pins. A trigger whose role is not button gets a development warning and keeps tap-only opening. A controlled layer reports the toggled value through onOpenChange (component:Tooltip FR9, FR10, AR6, DEC-1; component:HoverCard FR9, FR10, AR3, DEC-1).
  • A pinned layer takes Escape first. spec:AST-003 FR4 and DEC-6: an unpinned Tooltip stays passive and never wins or blocks Escape; a Tooltip or HoverCard pinned by a press is an active owner, so Escape closes it before the Dialog beneath it.
  • A closing pinned layer gives up Escape. spec:AST-090 FR5: a leaving unpinned Tooltip stays passive; a pinned Tooltip or HoverCard that is closing plays its exit and leaves the dismissal stack when the exit starts, so the member beneath takes the next Escape. Its layer-runtime and overlay-dismissal impact lines carry the same exception.
  • InfoTip is the info preset. New component:InfoTip in Core: a named button with the registry's info icon, wrapped in Tooltip with pressTrigger="toggle", with no layer, listener, or dismissal rule of its own. Props: content, label (a translated "More information" by default), and size (sm). It is one API with Tooltip under INV21, as MoreMenu is with DropdownMenu.
  • A tap that already acts stays with its control. Components declare pressTrigger="toggle" where the layer is the only home of their information. Under touchTrigger="auto", the fallback until the fold, a tap that already acts does not open the layer: walking out from the tapped element, the innermost control decides (a button, link, label, form control, option, menu item, or clickable row or card, the trigger included), and a disabled or aria-disabled control is not an action, so its reason opens on a tap. A role-less onClick element needs a role (component:Tooltip FR5, DEC-1; component:HoverCard FR5, DEC-1).
  • Action triggers never open on a tap. architecture:interaction-modality INV4: a tooltip that labels an action trigger whose accessible name already carries its text is not a separate interaction, so a tap performs the action; hint-only information is reachable on touch through pressTrigger="toggle".
  • Own-text triggers may toggle without being buttons. A trigger whose tooltip repeats its own visible text, such as a truncated Text that is its own tab stop, may take pressTrigger="toggle" with no development warning, no aria-expanded or aria-controls, and no announcement (component:Tooltip FR9).
  • FieldLabel's info icon becomes an InfoTip. labelTooltip renders an InfoTip after the label element, outside it, named with the label text ("About Auto-save"). FR9 and DEC-1 are amended in place: a tap, click, Enter, or Space on it toggles the help and never activates, focuses, or toggles the control, reversing the mouse-click activation FR9 kept. With a labelTooltip, one inline-flex row wraps the label element and the InfoTip, as FieldLabel already wraps the label and description; callers see that extra wrapper only when labelTooltip is set, and their class, xstyle, and ref stay on the label element. With isLabelHidden, the InfoTip is visually hidden with the label, takes no layout space, and becomes visible while it has keyboard focus. FR8, AR5, and the mouse-click part of FR9 stay implementation gaps until this is built (component:FieldLabel FR8, FR9, AR5, DEC-1).
  • StatusDot declares the toggle when standalone. A standalone dot with a tooltip becomes a button named by its label inside Tooltip with pressTrigger="toggle", adopted no later than FR5's walk ships, so its tooltip is reachable on touch and by keyboard; under a coarse pointer it reaches 44 px through the shared invisible hit area where its neighbors leave room (AST-094 FR42, component:StatusDot AR2). A dot inside a button, link, menu item, or option stays an image whose tooltip previews on hover only; sighted touch users lose its visible tooltip text there, and its status reaches screen-reader users through the host's accessible name, since a control cannot nest another control; a host named by aria-label includes the status in that label (component:StatusDot FR5, AR1, AR2).
  • Overlay dismissal follows. family:overlay-dismissal FR8 and its Escape table: an unpinned Tooltip never wins or blocks Escape; a pinned Tooltip or HoverCard takes it first.

Migration notes

  • FR5's walk (under the default touchTrigger="auto", a tap that already acts on a control around the trigger does not open the layer) needs app code where a hint-only trigger sits inside a control, so it is breaking under AST-017 FR1 and ships with CLN-0024 in the next minor. No caller's behavior changes in a patch: until then auto keeps its released behavior, except that a disabled control's reason opens on a tap, which ships separately as fix(Tooltip, HoverCard): a disabled control's reason opens on a tap #8207's patch [fix]. Hint-only triggers inside controls migrate by declaring pressTrigger="toggle"; InfoTip, FieldLabel's info button, and StatusDot do so before the minor.

  • touchTrigger="tap" is deprecated in the same release that adds pressTrigger (component:Tooltip DEP-0016), with static @deprecated metadata and no runtime warning. When both are given, pressTrigger="toggle" wins.

  • A codemod ships through the existing astryx upgrade path, staged in packages/cli/assets/codemods/transforms/next/ with tests and fixtures and checked by the codemod verify workflow, in the release that deprecates tap: it rewrites touchTrigger="tap" to pressTrigger="toggle" only where the trigger is statically a non-interactive icon or span, where the result is observably the same because Tooltip FR9 keeps a non-button toggle trigger on tap-only opening; it reports every other tap site (an own-text trigger, a button, a link, or an unknown element) with a manual-migration note and leaves it unchanged, since the toggle changes what a click, Enter, or Space does there (and on a button adds aria-expanded); it removes touchTrigger="auto" and reports spreads and dynamic values. The deprecated tap keeps working until the minor.

  • At the next minor, touchTrigger folds into pressTrigger and is removed (CLN-0024): pressTrigger takes auto | toggle | none, default auto, where auto is today's touchTrigger="auto" and none is today's touchTrigger="none". Each remaining touchTrigger="tap" site gets a named replacement there: a button or link whose only job is its tooltip takes pressTrigger="toggle" (it keeps its touch path); a button or link with its own action whose accessible name already carries the layer's text drops tap and takes auto (a tap performs the action and opens no layer); any other button or link with its own action moves the layer's information to an adjacent InfoTip (or another info trigger with pressTrigger="toggle") or into its visible label, then drops tap; a trigger whose tooltip repeats its own text takes pressTrigger="toggle"; an unknown element is reported for a manual choice. That minor's codemod rewrites touchTrigger="none" to pressTrigger="none", removes touchTrigger="auto" and today's explicit pressTrigger="none", keeps pressTrigger="toggle", rewrites own-text sites it can identify, reports every button and link tap site with all three options in its note (no static check tells them apart), and reports every other remaining tap site and dynamic value.

  • Lab InfoTip callers change the import to @astryxdesign/core; the Lab export stays one release as a deprecated re-export.

Rejected alternatives

  • A new general Toggletip component: its hover preview of text or a card would repeat Tooltip and HoverCard (INV21).
  • Widening touchTrigger="tap" to also pin on a mouse click and keys: a touch-named prop would then govern mouse and keyboard.

Claim scope and non-goals

  • Records only: no implementation, docs, or codemod in this pull request; consumer docs change with the build.
  • StatusDot's adoption is recorded here; any other component adopting pressTrigger="toggle" is a follow-up under its own record.
  • Not covered: a truncated label inside a link or button, where a tap belongs to the control.

Evidence and implementation relationship

  • The owner chose this option from a comparison of three: this one, the toggle on Tooltip and HoverCard without InfoTip, and a new general Toggletip. The records stand without that comparison.
  • Implementation follows in separate pull requests; every new requirement is marked as an implementation gap with an evidence gap in its verification row until then.

Writing rubric results

Run over component:Tooltip, component:HoverCard, component:FieldLabel, component:StatusDot, the new component:InfoTip, spec:AST-003, architecture:interaction-modality, family:overlay-dismissal, and spec:AST-090, and spec:AST-082 (FR3's example now names pressTrigger).

  • R1 narration: AST-090's amended lines state the system in the present tense; one found elsewhere, FieldLabel's compatibility line said a press "no longer" activates the control; rewritten as "does not". The Compatibility lines compare FR5 with the released auto, which the template asks for. DEP-0016's state: proposed is the lifecycle field value, not narration.
  • R2 research in the record: none; the options comparison lives in the linked artifact, and each record keeps one sentence of conclusion inside its DEC.
  • R3 evidence naming a change: none; new basis cells cite DEC-1, WCAG, WAI-ARIA, and the HTML label content model, and new verification cells name test files or an evidence gap.
  • R4 rejected alternatives over one line or outside a decision: six, each one line inside its decision (two in Tooltip DEC-1, one each in HoverCard DEC-1, InfoTip DEC-1, FieldLabel DEC-1 (amended), and AST-003 DEC-6).
  • R5 pull request or issue references remaining: none in the records.
  • R6 ruled open questions still listing options: none; FieldLabel's OQ1 is unrelated, unanswered, and unchanged.

Scope

  • This PR contains specification records only.
  • It records observable behavior and preserves equivalent internal implementations.
  • Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
  • Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
  • It records one intentional durable decision, not a rescue for a separable tagalong.
  • The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
  • Public text and artifacts contain no internal Meta context.

Validation

  • pnpm check:knowledge: "Knowledge templates and records are aligned."
  • pnpm check:repo: passes.
  • Prettier on the ten records.
  • git diff --check: clean.

@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Error Error Oct 31, 2026 11:20pm UTC

Request Review

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 3f754ca

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@astracat-bot

astracat-bot Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 31, 2026, 11:29 PM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 31, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec d6f2043

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 2b5215d

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec d6ea116

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the press-to-pin direction is approved for this head, but four record issues need fixing before it can merge. Please rebase onto main and fix the points below; the rebased head will need a fresh /approve-spec.

  • FieldLabel.spec.md FR9 and DEC-1 reuse the IDs that #9089 just made current on main with the opposite mouse-click rule, and the branch no longer merges cleanly.
  • docs/families/overlay-dismissal.md FR8 still says a Tooltip never wins or blocks Escape, which contradicts the new AST-003 FR4 and DEC-6 for a pinned Tooltip.
  • The Tooltip and HoverCard compatibility sections, and CLN-0024, describe the default as unchanged, but FR5 changes what a tap does under touchTrigger="auto".
  • The new HoverCard.doc.mjs guidance describes the innermost-control and disabled-control tap rule, which the shipped component does not follow yet.

[Automated review]

## Decision log

No component-local decision. The label-element and ref typing is OQ1.
### DEC-1 — The info trigger is an InfoTip beside the label

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main now has a different FieldLabel FR9 and DEC-1 (from #9089) that keep native label activation for a mouse click. After the rebase, replace those entries with this rule, or renumber these (FR10, DEC-2) and mark main's mouse-click clause superseded, so the record states one rule. Current-state impact (line 175) and the FR9 verification row need the same update.

- **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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/families/overlay-dismissal.md FR8 on main still says "A Tooltip never wins or blocks an Escape press against another member (spec:AST-003 FR4)", and its matrix rows give Escape to the Dialog over a Tooltip. Please amend FR8 and those rows to except a Tooltip pinned by a press.

targets, and public API remain unchanged
- Released default preserved: `yes`; `pressTrigger` defaults to `none`, which
keeps every shipped behavior
- Compatibility class: additive `pressTrigger`; `touchTrigger="tap"` is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FR5 and DEC-1 change the default touchTrigger="auto": a trigger inside a link or clickable row stops opening on the tap, and a disabled control's reason starts opening on one. Without pressTrigger the runtime does change, so this line and HoverCard's should say so with its spec:AST-017 class. CLN-0024's delta (line 100) also still describes the trigger-only rule.

guidance: true,
description:
'Leave touchTrigger on auto so a tap opens the card on triggers that do nothing else, and stays out of the way on triggers that perform an action.',
'Leave touchTrigger on auto, the fallback: a tap that already acts, as the innermost control around the tapped element decides, stays with that control, and a disabled control is not an action, so its reason opens on a tap. A clickable element without a role needs one.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shipped isActionTrigger (packages/core/src/Layer/useTouchTrigger.ts) reads only the trigger and treats a disabled <button> as an action, so this guidance describes behaviour HoverCard does not have yet (FR5 lists it as an implementation gap). Keep the current wording here and in the two locale copies until the walk ships.

@cixzhang
cixzhang force-pushed the spec/press-trigger-toggle branch from d6ea116 to c2e5c15 Compare October 31, 2026 18:53
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec c2e5c15

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 63115d1

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC
Field 45.8 kB (0) · 8.3 kB CSS (0) —
FieldLabel 39.6 kB (0) · 5.9 kB CSS (0) —
HoverCard 17.7 kB (0) · 5.0 kB CSS (0) —
2 more components
Component Size LoC
StatusDot 19.6 kB (0) · 6.3 kB CSS (0) —
Tooltip 17.4 kB (0) · 5.0 kB CSS (0) —

Package size: @astryxdesign/core 6.54 MB packed (+17.1 kB); no new dependencies.

Not run on this PR: add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the press-to-pin direction is approved for this head and the earlier record issues are fixed, but the default touchTrigger="auto" change needs a different release class. Please address the points below; the new head will need a fresh /approve-spec.

  • FR5's walk is filed as an AST-017 FR53 patch fix in the Tooltip and HoverCard Compatibility sections, but FR53 excludes changes that need app code, and a hint-only trigger inside a clickable row, card, link or label would need pressTrigger="toggle" to stay reachable on touch; classify that part under FR1 with CLN-0024's minor.
  • Core StatusDot uses the default auto, so its FR5 promise that the tooltip opens on touch stops holding inside a control; adopt the toggle there no later than the walk ships and amend StatusDot FR5.
  • The description's user impact names status dots and timestamps, which these records do not change; narrow it to InfoTip and FieldLabel's info button.

[Automated review]

- Compatibility class: records shipped behavior; runtime, DOM, styling,
targets, and public API remain unchanged
- Released default preserved: the defaults stay `pressTrigger="none"` and
`touchTrigger="auto"`, but FR5 changes what a tap does under `touchTrigger="auto"`: a tap that already acts does not open the layer, where the released `auto` opens it on any trigger that does not itself act, and a disabled control's reason opens on a tap. That change ships in a patch as `[fix]` under `spec:AST-017` FR53, because it restores `architecture:interaction-modality` INV4 (a tap on an action trigger performs the action, and every modality reaches the reason); a tap destination INV4 does not cover would ship as a `[feat]` patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-017 FR53 covers only a fix that "requires no app-layer code change"; a change that needs app edits "is classified under FR1". The released auto opens on a tap for any trigger that does not itself act, so a hint-only trigger inside a clickable row, card, link or label opens today. Under FR5 it stays shut, and only a button trigger with pressTrigger="toggle" brings the touch path back. Core StatusDot (default auto) is one such caller, and its FR5 says its tooltip opens on touch. Please classify the inside-a-control walk under FR1 with CLN-0024's minor, keeping the disabled-reason part as the FR53 patch.

- Released default preserved: `yes`
- Compatibility class: records shipped behavior; no public change
- Released default preserved: the defaults stay `pressTrigger="none"` and
`touchTrigger="auto"`, but FR5 changes what a tap does under `touchTrigger="auto"`: a tap that already acts does not open the layer, where the released `auto` opens it on any trigger that does not itself act, and a disabled control's reason opens on a tap. That change ships in a patch as `[fix]` under `spec:AST-017` FR53, because it restores `architecture:interaction-modality` INV4 (a tap on an action trigger performs the action, and every modality reaches the reason); a tap destination INV4 does not cover would ship as a `[feat]` patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same release-class issue as Tooltip's Compatibility line: a hint-only card trigger inside a clickable row or card loses its tap under FR5, and only app code (pressTrigger="toggle" on a button trigger) restores it, which AST-017 FR53 excludes from a patch fix.

| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | `CLN-0024` |
| lifecycle | `DEP-0016` |
| delta | `touchTrigger` is removed from `Tooltip`, `HoverCard`, `useTooltip`, and `useHoverCard`. `pressTrigger` takes `auto`, `toggle`, and `none`, default `auto`: `auto` is the released `touchTrigger="auto"` (a tap opens the layer unless the trigger performs its own action, and no press pins it), `none` is the released `touchTrigger="none"` (no tap or press opens it), and `toggle` is unchanged. `auto` keeps FR5's rule that a tap that already acts does not open the layer, which ships earlier as a patch (Compatibility and migration). One prop decides what a press does |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This defines auto as "a tap opens the layer unless the trigger performs its own action", the released rule FR5 replaces, and then says auto keeps FR5's rule. Please describe the rule auto will actually carry after the fold.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 7e15ea5

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
…ay-dismissal FR8; FR5 auto change is a patch fix; revert HoverCard docs
…hosts carry the status; pinned layers in overlay-dismissal rows
… hit area where neighbors leave room; unescape INV4 row
… lines); InfoTip hit area where neighbors leave room
@cixzhang
cixzhang force-pushed the spec/press-trigger-toggle branch from e36deb8 to b67fa3a Compare October 31, 2026 22:37
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec b67fa3a

@github-actions github-actions Bot added needs:spec-owner-review Current knowledge records await owner approval and removed needs:spec-owner-review Current knowledge records await owner approval labels Oct 31, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the press-to-pin direction is owner-approved for this head and the previous round's CLN-0024 and Builder impact points are fixed, but the new button rule takes away the touch path this pull request sets out to add. CLN-0024 drops touchTrigger="tap" from every button or link trigger at the next minor (Tooltip.spec.md:109-110), yet tap is how a button whose only job is its tooltip opens on a tap today (the Lab InfoTip and the Tooltip story do this), and released auto treats any button as an action, so that help stops opening on touch, which architecture:interaction-modality INV4 rules out. Please name pressTrigger="toggle" as the replacement for such buttons, have the minor's codemod report button and link tap sites instead of removing tap, and fix the codemod verification row (:302), which fails the minor's own rewrites; once fixed, an engineering owner records the decision on the new head with /approve-spec <full head SHA>, which the author can post as an engineering owner.

[Automated review]

| id | `CLN-0024` |
| lifecycle | `DEP-0016` |
| delta | `touchTrigger` is removed from `Tooltip`, `HoverCard`, `useTooltip`, and `useHoverCard`. `pressTrigger` takes `auto`, `toggle`, and `none`, default `auto`: `auto` is the released `touchTrigger="auto"` (a tap opens the layer unless the trigger performs its own action, and no press pins it), `none` is the released `touchTrigger="none"` (no tap or press opens it), and `toggle` is unchanged. `auto` gains FR5's walk: a tap that already acts on a control around the trigger does not open the layer. A hint-only trigger inside a control migrates by declaring `pressTrigger="toggle"`, which no codemod can decide; Astryx's own info-only triggers (InfoTip, FieldLabel's info button, and a standalone StatusDot) declare it before this minor; a StatusDot inside a button, link, menu item, or option stays an image whose status the host's accessible name carries. One prop decides what a press does |
| replacement | Each remaining `touchTrigger="tap"` site has a named replacement (`spec:AST-017` FR8): a button or link trigger drops it and takes `auto`, so a tap performs the action and opens no tooltip (`architecture:interaction-modality` INV4); a trigger whose tooltip repeats its own text takes `pressTrigger="toggle"` (FR9's own-text exception); an unknown element is reported for a manual choice between those two |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A button whose only job is its tooltip (the Lab InfoTip's trigger, the Tooltip story's "?" button) carries tap because released auto treats a <button> as an action. Dropping tap there leaves its help with no touch path, against architecture:interaction-modality INV4, DEP-0016's replacement (:92) and this record's own delta (:108). Please name pressTrigger="toggle" for a button whose only job is the layer, keep "drop it" for a button that acts and whose name carries the text, and have the codemod (:110) report button and link sites instead of removing tap.

| AR5 | `Tooltip.a11y.test.tsx`; `Tooltip.a11y.chromium.spec.ts` | Wrapped, text, anchor, and hook triggers | A lost role, description, keyboard-focus show, or Escape dismissal fails; focus-leave hide, hover show, and leave hide run as advisory rows (Current-state impact). | `audit:Tooltip/accessibility` |
| FR5 | `Tooltip.test.tsx` touch suite; `TouchTrigger.a11y.chromium.spec.ts`; `useTouchTrigger.test.ts`, with tap-destination cases an evidence gap until built | Tap open, scroll, second tap, tap outside; a trigger inside a link, a clickable row, or a clickable card; a trigger in running text; a disabled control inside a clickable row | A tap that fails to open or close as stated, opens the tooltip on a tap that already acts, or leaves a disabled control's reason shut fails. | `audit:Tooltip/behavior` |
| FR9, FR10, AR6 | Evidence gap until built: a press-toggle suite in `Tooltip.test.tsx` and `useTooltip.test.tsx`, and a real-touch and keyboard case in `TouchTrigger.a11y.chromium.spec.ts` | Click, Enter, Space, and tap pin; second press, press outside, focus leaving, and Escape close; pointer leave keeps the pin; controlled; non-button trigger; a truncated Text trigger whose tooltip repeats its text; a toggle trigger inside a ClickableCard, an Item row, and a `<label>` | A press that does not pin or close as stated, a pin lost on pointer leave, a missing or stale `aria-expanded` or `aria-controls`, a second announcement, a non-button trigger without the warning or its tap fallback, an own-text trigger that warns, gets `aria-expanded`, or is announced, or a pinning press that also activates the card, row, or labelled control around the trigger, fails. | `audit:Tooltip/behavior` |
| DEP-0016, CLN-0024 | Evidence gap until built: the codemod's tests and fixtures beside its transform, and the codemod verify workflow | `touchTrigger="tap"` on an icon and a span, and on an own-text trigger, a button, a link, and an unknown element; `"auto"` and `"none"`; `pressTrigger` beside `touchTrigger`; spreads and dynamic values | A rewrite that changes behavior, a rewritten own-text, button, link, or unknown-element site, a reported site without its manual-migration note, a missed static use, or a silently skipped dynamic one fails. | `audit:Tooltip/api` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row covers both codemods, but CLN-0024's codemod rewrites button, link and own-text sites by design (:110), so "a rewritten own-text, button, link, or unknown-element site ... fails" fails it. Please split the row by release, and add a failure for a remaining tap the minor neither rewrites nor reports.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 63115d1

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The press-to-pin direction is owner-approved for this head, and the info-only button rule and codemod verification row now read correctly; one record rule still needs a change. CLN-0024 (Tooltip.spec.md:109-110) tells every button or link with its own action to drop touchTrigger="tap" for auto, but the INV4 sentence this pull request adds exempts only triggers whose accessible name already carries the layer's text, so a tooltip hint or hover card on any other action trigger loses its only touch path. Please limit that replacement to INV4's case and name one that keeps a touch path for the rest (for example an adjacent InfoTip) in both the replacement and the minor's codemod note, then post a fresh /approve-spec for the new head; smaller wording fixes are listed in the review.

[Automated review]

| id | `CLN-0024` |
| lifecycle | `DEP-0016` |
| delta | `touchTrigger` is removed from `Tooltip`, `HoverCard`, `useTooltip`, and `useHoverCard`. `pressTrigger` takes `auto`, `toggle`, and `none`, default `auto`: `auto` is the released `touchTrigger="auto"` (a tap opens the layer unless the trigger performs its own action, and no press pins it), `none` is the released `touchTrigger="none"` (no tap or press opens it), and `toggle` is unchanged. `auto` gains FR5's walk: a tap that already acts on a control around the trigger does not open the layer. A hint-only trigger inside a control migrates by declaring `pressTrigger="toggle"`, which no codemod can decide; Astryx's own info-only triggers (InfoTip, FieldLabel's info button, and a standalone StatusDot) declare it before this minor; a StatusDot inside a button, link, menu item, or option stays an image whose status the host's accessible name carries. One prop decides what a press does |
| replacement | Each remaining `touchTrigger="tap"` site has a named replacement (`spec:AST-017` FR8): a button or link whose only job is its tooltip takes `pressTrigger="toggle"`, so its tooltip keeps a touch path; a button or link with its own action drops `tap` and takes `auto`, so a tap performs the action and opens no tooltip; a trigger whose tooltip repeats its own text takes `pressTrigger="toggle"` (FR9's own-text exception); an unknown element is reported for a manual choice (`architecture:interaction-modality` INV4) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"A button or link with its own action drops tap and takes auto" also covers action triggers whose tooltip or hover card holds content their accessible name does not carry (an icon button named "Sync" whose tooltip says "Last synced 5 minutes ago", a name link with a profile card). The released tap (:91) is their only touch path, and the INV4 sentence this pull request adds exempts only "a tooltip that labels an action trigger whose accessible name already carries its text". Could this row limit the drop to that case and name a replacement that keeps a touch path for the rest (for example, move the content to an adjacent InfoTip), with the codemod note on :110 offering it?

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 4d77b9f

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The press-to-pin direction is owner-approved for this head, and CLN-0024 now gives every remaining touchTrigger="tap" site a replacement that keeps a touch path, so these records are ready to merge. A few small wording follow-ups can ride the implementation pull requests:

  • CLN-0024's codemod note offers HoverCard sites an adjacent InfoTip, which only holds short, noninteractive help; the replacement row's "adjacent info trigger that declares pressTrigger="toggle"" fits both layers.
  • component:Field's theming anatomy still delegates the label tooltip trigger to Icon/icon, which changes once FieldLabel's InfoTip button is built.

[Automated review]

| lifecycle | `DEP-0016` |
| delta | `touchTrigger` is removed from `Tooltip`, `HoverCard`, `useTooltip`, and `useHoverCard`. `pressTrigger` takes `auto`, `toggle`, and `none`, default `auto`: `auto` is the released `touchTrigger="auto"` (a tap opens the layer unless the trigger performs its own action, and no press pins it), `none` is the released `touchTrigger="none"` (no tap or press opens it), and `toggle` is unchanged. `auto` gains FR5's walk: a tap that already acts on a control around the trigger does not open the layer. A hint-only trigger inside a control migrates by declaring `pressTrigger="toggle"`, which no codemod can decide; Astryx's own info-only triggers (InfoTip, FieldLabel's info button, and a standalone StatusDot) declare it before this minor; a StatusDot inside a button, link, menu item, or option stays an image whose status the host's accessible name carries. One prop decides what a press does |
| replacement | Each remaining `touchTrigger="tap"` site has a named replacement (`spec:AST-017` FR8): a button or link whose only job is its tooltip takes `pressTrigger="toggle"`, so its tooltip keeps a touch path; a button or link with its own action whose accessible name already carries the layer's text drops `tap` and takes `auto`, so a tap performs the action and opens no layer (`architecture:interaction-modality` INV4's case); any other button or link with its own action moves the layer's information to an adjacent info trigger that declares `pressTrigger="toggle"`, such as an InfoTip, or into its visible label, so that information keeps a touch path, and then drops `tap`; a trigger whose tooltip repeats its own text takes `pressTrigger="toggle"` (FR9's own-text exception); an unknown element is reported for a manual choice (`architecture:interaction-modality` INV4) |
| codemod | The minor's `astryx upgrade` codemod, with tests and fixtures: `touchTrigger="none"` becomes `pressTrigger="none"`; `touchTrigger="auto"` and the released `pressTrigger="none"` are removed, since `auto` reproduces them; `pressTrigger="toggle"` is kept and any `touchTrigger` beside it removed; a remaining `touchTrigger="tap"` becomes `pressTrigger="toggle"` on an own-text trigger the codemod can identify; every button and link `tap` site is reported with the options in its note (`pressTrigger="toggle"` when the trigger's only job is its layer; remove `tap` for `auto` when the trigger's accessible name already carries the layer's text; otherwise move the information to an adjacent InfoTip or the visible label, then remove `tap`), since no static check tells them apart, and every other remaining `tap` site is reported; spreads and dynamic values are reported |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: HoverCard DEC-1 applies CLN-0024 unchanged, so a HoverCard tap site on an action link gets this note too, but "an adjacent InfoTip" cannot hold a card, since InfoTip takes only short, noninteractive help (InfoTip FR3). The replacement row's wording, "an adjacent info trigger that declares pressTrigger="toggle"", covers both layers; could the note use it?

This branch had an error being deployed

1 failed deployment
Preview — 4d77b9fd Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant