Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| LayerProvider | 68.8 kB (+1.7 kB) · 10.4 kB CSS (0) | — | ✓ · ✓ · ✗ |
| lab/DiffViewer | 🔴 50.6 kB (+3.4 kB) · 2.1 kB CSS (0) | — | — · — · — |
| lab/Schedule | 🔴 67.1 kB (+3.3 kB) · 3.9 kB CSS (0) | — | — · — · — |
122 more components
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| lab/TourStep | 🔴 61.2 kB (+3.2 kB) · 590 B CSS (0) | — | — · — · — |
| Popover | 🔴 55.8 kB (+3.2 kB) · 12.4 kB CSS (0) | — | — · — · — |
| Carousel | 🔴 56.7 kB (+3.2 kB) · 10.5 kB CSS (0) | — | — · — · — |
| DialogHeader | 🔴 54.5 kB (+2.7 kB) · 10.1 kB CSS (0) | — | — · — · — |
| DrawerHeader | 🔴 54.6 kB (+2.7 kB) · 11.5 kB CSS (0) | — | — · — · — |
| TableSelectionToolbar | 🔴 57.9 kB (+2.6 kB) · 11.4 kB CSS (0) | — | — · — · — |
| ChatLayout | 🔴 54.6 kB (+2.6 kB) · 10.2 kB CSS (0) | — | — · — · — |
| Stepper | 🔴 53.1 kB (+2.6 kB) · 9.9 kB CSS (0) | — | — · — · — |
| ChatDictationButton | 🔴 52.0 kB (+2.6 kB) · 9.8 kB CSS (0) | — | — · — · — |
| MobileNavToggle | 🔴 51.8 kB (+2.6 kB) · 9.7 kB CSS (0) | — | — · — · — |
| Banner | 🔴 53.3 kB (+2.6 kB) · 10.2 kB CSS (0) | — | — · — · — |
| TopNavMegaMenu | 🔴 60.0 kB (+2.6 kB) · 11.1 kB CSS (0) | — | — · — · — |
| ChatSendButton | 🔴 51.3 kB (+2.6 kB) · 9.4 kB CSS (0) | — | — · — · — |
| TopNavMenu | 🔴 63.6 kB (+2.6 kB) · 10.5 kB CSS (0) | — | — · — · — |
| ToggleButton | 🔴 49.4 kB (+2.5 kB) · 9.5 kB CSS (0) | — | — · — · — |
| ChatLayoutScrollButton | 🔴 52.0 kB (+2.5 kB) · 9.9 kB CSS (0) | — | — · — · — |
| SideNavCollapseButton | 🔴 52.2 kB (+2.5 kB) · 9.8 kB CSS (0) | — | — · — · — |
| SideNavHeading | 🔴 64.1 kB (+2.5 kB) · 10.5 kB CSS (0) | — | — · — · — |
| lab/InfoTip | 🔴 25.5 kB (+2.5 kB) · 458 B CSS (0) | — | — · — · — |
| SideNavItem | 🔴 60.3 kB (+2.5 kB) · 10.4 kB CSS (0) | — | — · — · — |
| Toast | 🔴 65.5 kB (+2.5 kB) · 10.0 kB CSS (0) | — | — · — · — |
| TopNavHeading | 🔴 63.5 kB (+2.5 kB) · 10.3 kB CSS (0) | — | — · — · — |
| lab/CommentPin | 🔴 52.9 kB (+2.5 kB) · 1.2 kB CSS (0) | — | — · — · — |
| StatusDot | 🔴 22.6 kB (+2.5 kB) · 6.3 kB CSS (0) | — | — · — · — |
| Tooltip · Storybook | 🔴 20.4 kB (+2.5 kB) · 5.0 kB CSS (0) | 627 (+50) | ✓ · ✓ · ✓ |
| AvatarGroupOverflow | 🔴 45.0 kB (+2.5 kB) · 6.5 kB CSS (0) | — | — · — · — |
| Button | 🔴 48.5 kB (+2.5 kB) · 9.4 kB CSS (0) | — | — · — · — |
| IconButton | 🔴 48.6 kB (+2.5 kB) · 9.4 kB CSS (0) | — | — · — · — |
| Link | 🔴 49.0 kB (+2.5 kB) · 7.9 kB CSS (0) | — | — · — · — |
| TabMenu | 🔴 60.8 kB (+2.5 kB) · 10.4 kB CSS (0) | — | — · — · — |
| FieldLabel | 🔴 42.5 kB (+2.5 kB) · 5.9 kB CSS (0) | — | — · — · — |
| Avatar | 🔴 41.6 kB (+2.5 kB) · 6.5 kB CSS (0) | — | — · — · — |
| AvatarGroup | 🔴 44.0 kB (+2.5 kB) · 6.6 kB CSS (0) | — | — · — · — |
| SegmentedControl | 🔴 43.0 kB (+2.4 kB) · 6.5 kB CSS (0) | — | — · — · — |
| AppShell | 🔴 73.9 kB (+2.2 kB) · 12.0 kB CSS (0) | — | — · — · — |
| AlertDialog | 🔴 62.0 kB (+2.2 kB) · 12.7 kB CSS (0) | — | — · — · — |
| SideNav | 🔴 69.9 kB (+2.1 kB) · 10.9 kB CSS (0) | — | — · — · — |
| ContextMenuItem | 🔴 58.5 kB (+2.0 kB) · 9.9 kB CSS (0) | — | — · — · — |
| DropdownMenuSubMenu | 🔴 63.8 kB (+2.0 kB) · 9.9 kB CSS (0) | — | — · — · — |
| MobileNav | 🔴 56.3 kB (+2.0 kB) · 10.4 kB CSS (0) | — | — · — · — |
| TopNav | 🔴 57.2 kB (+2.0 kB) · 10.7 kB CSS (0) | — | — · — · — |
| Lightbox | 56.5 kB (+1.9 kB) · 10.1 kB CSS (0) | — | — · — · — |
| CodeBlock | 65.1 kB (+1.9 kB) · 11.0 kB CSS (0) | — | — · — · — |
| TimeInput | 94.1 kB (+1.9 kB) · 14.1 kB CSS (0) | — | — · — · — |
| charts/ChartLegend | 58.4 kB (+1.9 kB) · 20 B CSS (0) | — | — · — · — |
| ChatComposerInput | 60.6 kB (+1.8 kB) · 10.3 kB CSS (0) | — | — · — · — |
| ChatComposerTokenElement | 60.6 kB (+1.8 kB) · 10.3 kB CSS (0) | — | — · — · — |
| charts/ChartTooltip | 58.3 kB (+1.8 kB) · 20 B CSS (0) | — | — · — · — |
| ContextMenu | 80.3 kB (+1.8 kB) · 15.2 kB CSS (0) | — | — · — · — |
| charts/Chart | 69.0 kB (+1.8 kB) · 20 B CSS (0) | — | — · — · — |
| TableCell | 81.5 kB (+1.8 kB) · 15.7 kB CSS (0) | — | — · — · — |
| DateTimeInput | 117.3 kB (+1.8 kB) · 15.6 kB CSS (0) | — | — · — · — |
| DateInput | 105.7 kB (+1.8 kB) · 15.1 kB CSS (0) | — | — · — · — |
| richtext/RichTextView | 255.4 kB (+1.8 kB) · 2.3 kB CSS (0) | — | — · — · — |
| TableHeaderCell | 81.1 kB (+1.8 kB) · 15.4 kB CSS (0) | — | — · — · — |
| Calendar | 60.0 kB (+1.8 kB) · 10.3 kB CSS (0) | — | — · — · — |
| BreadcrumbItem | 72.5 kB (+1.8 kB) · 11.4 kB CSS (0) | — | — · — · — |
| CheckboxList | 49.9 kB (+1.8 kB) · 8.5 kB CSS (0) | — | — · — · — |
| DropdownMenu | 78.3 kB (+1.8 kB) · 15.4 kB CSS (0) | — | — · — · — |
| TextArea | 53.4 kB (+1.8 kB) · 9.7 kB CSS (0) | — | — · — · — |
| Field | 48.1 kB (+1.8 kB) · 8.3 kB CSS (0) | — | — · — · — |
| Thumbnail | 55.5 kB (+1.8 kB) · 11.3 kB CSS (0) | — | — · — · — |
| lab/TransferListSelector | 77.9 kB (+1.8 kB) · 1.9 kB CSS (0) | — | — · — · — |
| FileInput | 59.3 kB (+1.8 kB) · 10.7 kB CSS (0) | — | — · — · — |
| lab/TransferList | 72.3 kB (+1.8 kB) · 1.7 kB CSS (0) | — | — · — · — |
| Switch | 53.1 kB (+1.8 kB) · 9.7 kB CSS (0) | — | — · — · — |
| richtext/RichTextEditor | 274.9 kB (+1.8 kB) · 2.6 kB CSS (0) | — | — · — · — |
| Slider | 52.8 kB (+1.7 kB) · 8.8 kB CSS (0) | — | — · — · — |
| InputGroup | 48.7 kB (+1.7 kB) · 8.3 kB CSS (0) | — | — · — · — |
| TextInput | 58.5 kB (+1.7 kB) · 10.8 kB CSS (0) | — | — · — · — |
| MoreMenu | 78.5 kB (+1.7 kB) · 15.4 kB CSS (0) | — | — · — · — |
| RadioList | 49.5 kB (+1.7 kB) · 8.3 kB CSS (0) | — | — · — · — |
| DateRangeInput | 74.8 kB (+1.7 kB) · 11.5 kB CSS (0) | — | — · — · — |
| NumberInput | 60.7 kB (+1.7 kB) · 11.0 kB CSS (0) | — | — · — · — |
| ComplexSelector | 61.2 kB (+1.7 kB) · 11.0 kB CSS (0) | — | — · — · — |
| lab/ChatEmojiPicker | 70.4 kB (+1.7 kB) · 773 B CSS (0) | — | — · — · — |
| lab/ChatReactionBar | 70.9 kB (+1.7 kB) · 1.1 kB CSS (0) | — | — · — · — |
| RadioListItem | 57.3 kB (+1.7 kB) · 10.0 kB CSS (0) | — | — · — · — |
| ListItem | 58.8 kB (+1.7 kB) · 11.0 kB CSS (0) | — | — · — · — |
| lab/EmojiPicker | 68.2 kB (+1.7 kB) · 722 B CSS (0) | — | — · — · — |
| CheckboxListItem | 59.3 kB (+1.7 kB) · 11.0 kB CSS (0) | — | — · — · — |
| CheckboxInput | 54.7 kB (+1.7 kB) · 9.6 kB CSS (0) | — | — · — · — |
| richtext/RichTextEditorToolbar | 218.6 kB (+1.5 kB) · 454 B CSS (0) | — | — · — · — |
| PowerSearch | 176.6 kB (+1.5 kB) · 20.3 kB CSS (0) | — | — · — · — |
| lab/MobileTokenizer | 97.8 kB (+1.4 kB) · 880 B CSS (0) | — | — · — · — |
| lab/CommentThread | 222.8 kB (+1.4 kB) · 2.4 kB CSS (0) | — | — · — · — |
| lab/MobilePowerSearch | 162.0 kB (+1.4 kB) · 1.2 kB CSS (0) | — | — · — · — |
| ChatComposer | 65.5 kB (+1.4 kB) · 11.1 kB CSS (0) | — | — · — · — |
| lab/Comments | 227.4 kB (+1.4 kB) · 2.5 kB CSS (0) | — | — · — · — |
| BaseTypeahead | 59.9 kB (+1.4 kB) · 10.2 kB CSS (0) | — | — · — · — |
| Tokenizer | 74.5 kB (+1.3 kB) · 16.9 kB CSS (0) | — | — · — · — |
| Pagination | 90.4 kB (+1.3 kB) · 15.7 kB CSS (0) | — | — · — · — |
| lab/ListInput | 79.8 kB (+1.3 kB) · 1.6 kB CSS (0) | — | — · — · — |
| Selector | 81.9 kB (+1.3 kB) · 15.4 kB CSS (0) | — | — · — · — |
| Markdown | 150.2 kB (+1.3 kB) · 18.2 kB CSS (0) | — | — · — · — |
| Table | 89.7 kB (+1.3 kB) · 15.9 kB CSS (0) | — | — · — · — |
| lab/CommentComposer | 99.1 kB (+1.3 kB) · 1.9 kB CSS (0) | — | — · — · — |
| MultiSelector | 77.8 kB (+1.2 kB) · 15.0 kB CSS (0) | — | — · — · — |
| Typeahead | 67.3 kB (+1.2 kB) · 11.6 kB CSS (0) | — | — · — · — |
| CommandPalette | 54.3 kB (+185 B) · 12.1 kB CSS (0) | — | — · — · — |
| CommandPaletteInput | 33.8 kB (+101 B) · 8.4 kB CSS (0) | — | — · — · — |
| ChatMessageList | 29.9 kB (+68 B) · 7.9 kB CSS (0) | — | — · — · — |
| HoverCard | 18.2 kB (+58 B) · 5.0 kB CSS (0) | — | — · — · — |
| DropdownMenuItem | 16.9 kB (+49 B) · 5.5 kB CSS (0) | — | — · — · — |
| DropdownMenuCheckboxItem | 17.7 kB (+45 B) · 5.9 kB CSS (0) | — | — · — · — |
| SelectorOption | 16.0 kB (+43 B) · 5.4 kB CSS (0) | — | — · — · — |
| TreeList | 43.2 kB (+38 B) · 5.0 kB CSS (0) | — | — · — · — |
| ClickableCard | 14.0 kB (+36 B) · 7.1 kB CSS (0) | — | — · — · — |
| Item | 12.7 kB (+36 B) · 4.9 kB CSS (0) | — | — · — · — |
| DropdownMenuRadioItem | 17.7 kB (+35 B) · 5.9 kB CSS (0) | — | — · — · — |
| SelectableCard | 13.8 kB (+33 B) · 7.3 kB CSS (0) | — | — · — · — |
| Overlay | 19.8 kB (+30 B) · 4.0 kB CSS (0) | — | — · — · — |
| Timer | 9.5 kB (+30 B) · 6.9 kB CSS (0) | — | — · — · — |
| ProgressBar | 8.0 kB (+28 B) · 6.9 kB CSS (0) | — | — · — · — |
| Token | 31.5 kB (+22 B) · 4.8 kB CSS (0) | — | — · — · — |
| ChatToolCalls | 32.9 kB (+19 B) · 9.2 kB CSS (0) | — | — · — · — |
| Spinner | 25.7 kB (+19 B) · 6.7 kB CSS (0) | — | — · — · — |
| Text | 7.4 kB (+1 B) · 6.0 kB CSS (0) | — | — · — · — |
| Heading | 7.9 kB (−3 B) · 6.0 kB CSS (0) | — | — · — · — |
| Timestamp | 28.4 kB (−7 B) · 9.9 kB CSS (0) | — | — · — · — |
| layer | — | — | — · ✓ · — |
| Layer | — | — | ✓ · ✓ · — |
Package size: @astryxdesign/core 5.67 MB packed (+14.3 kB); no new dependencies.
API changes: core +12 · ~20 · −0 (report stale: run pnpm api:report); richtext +0 · ~1 · −0 (report stale: run pnpm api:report)
33 API changes (advisory)
| Package | Where | Signature | Usage | |
|---|---|---|---|---|
| + | core |
. · Components · component Tooltip |
prop pressTrigger?: TooltipPressTrigger = 'none' |
<Tooltip pressTrigger="none" /> |
| + | core |
. · Types · interface FieldStatusProps |
message: string |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {message: …} |
| + | core |
. · Types · interface FieldStatusProps |
ref?: React.Ref<HTMLDivElement> |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {ref: …} |
| + | core |
. · Types · interface FieldStatusProps |
type: InputStatusType |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {type: …} |
| + | core |
. · Types · interface FieldStatusProps |
variant?: FieldStatusVariant = 'attached' |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {variant: …} |
| + | core |
. · Types · interface FieldStatusProps |
xstyle?: StyleXStyles |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {xstyle: …} |
| + | core |
. · Types · interface FieldStatusProps |
inherits AriaAttributes, DOMAttributes, HTMLAttributes |
import type {FieldStatusProps} from '@astryxdesign/core' |
| + | core |
. · Types · interface SearchContext |
interface SearchContext<T extends SearchableItem = SearchableItem> |
import type {SearchContext} from '@astryxdesign/core' |
| + | core |
. · Types · interface TooltipOptions |
pressTrigger?: TooltipPressTrigger = 'none' |
import type {TooltipOptions} from '@astryxdesign/core'; const x: TooltipOptions = {pressTrigger: …} |
| + | core |
. · Types · type TooltipPressTrigger |
type TooltipPressTrigger = LayerPressTrigger |
import type {TooltipPressTrigger} from '@astryxdesign/core' |
| + | core |
./Tooltip · Also exported from an earlier entry point · TooltipPressTrigger |
TooltipPressTrigger (.) |
import {TooltipPressTrigger} from '@astryxdesign/core/Tooltip' |
| + | core |
./Typeahead · Also exported from an earlier entry point · SearchContext |
SearchContext (.) |
import {SearchContext} from '@astryxdesign/core/Typeahead' |
| ~ | core |
. · Components · component ComplexSelector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary and ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary and ghost selectors |
<ComplexSelector statusVariant="attached" /> |
| ~ | core |
. · Components · component DateInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<DateInput statusVariant="attached" /> |
| ~ | core |
. · Components · component DateRangeInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<DateRangeInput statusVariant="attached" /> |
| ~ | core |
. · Components · component Field |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Field statusVariant="attached" /> |
| ~ | core |
. · Components · component FieldStatus |
component FieldStatus(props: FieldStatusProps) → component FieldStatus @deprecated |
<FieldStatus /> |
| ~ | core |
. · Components · component FieldStatus |
prop variant?: FieldStatusVariant = 'attached' → prop variant: FieldStatusVariant = 'attached' |
<FieldStatus variant="attached" /> |
| ~ | core |
. · Components · component FileInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<FileInput statusVariant="attached" /> |
| ~ | core |
. · Components · component MultiSelector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary and ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary and ghost selectors |
<MultiSelector statusVariant="attached" /> |
| ~ | core |
. · Components · component NumberInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<NumberInput statusVariant="attached" /> |
| ~ | core |
. · Components · component PowerSearch |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<PowerSearch statusVariant="attached" /> |
| ~ | core |
. · Components · component Selector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary/ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary/ghost selectors |
<Selector statusVariant="attached" /> |
| ~ | core |
. · Components · component TextArea |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TextArea statusVariant="attached" /> |
| ~ | core |
. · Components · component TextInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TextInput statusVariant="attached" /> |
| ~ | core |
. · Components · component TimeInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TimeInput statusVariant="attached" /> |
| ~ | core |
. · Components · component Tokenizer |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Tokenizer statusVariant="attached" /> |
| ~ | core |
. · Components · component Typeahead |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Typeahead statusVariant="attached" /> |
| ~ | core |
. · Types · interface FieldStatusProps |
interface FieldStatusProps extends BaseProps<HTMLDivElement> (props of FieldStatus) → interface FieldStatusProps extends BaseProps<HTMLDivElement> |
import type {FieldStatusProps} from '@astryxdesign/core' |
| ~ | core |
. · Types · interface FieldStatusVariantMap |
tooltip: true → tooltip: true @deprecated |
import type {FieldStatusVariantMap} from '@astryxdesign/core'; const x: FieldStatusVariantMap = {tooltip: …} |
| ~ | core |
. · Types · interface SearchSource |
search: (query: string) => Promise<T[]> | T[] → search: (query: string, context?: SearchContext<T>) => Promise<T[]> | T[] |
import type {SearchSource} from '@astryxdesign/core'; const x: SearchSource = {search: …} |
| ~ | core |
. · Types · interface UseInputStatusIconOptions |
statusVariant?: FieldStatusVariant → statusVariant?: FieldStatusPlacement |
import type {UseInputStatusIconOptions} from '@astryxdesign/core'; const x: UseInputStatusIconOptions = {statusVariant: …} |
| ~ | richtext |
. · Components · component RichTextEditor |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusVariant | 'tooltip' = 'attached' |
<RichTextEditor statusVariant="tooltip" /> |
Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.
Generated by PR Enrichment workflow | View full report
There was a problem hiding this comment.
Changes requested: the toggle behaves as component:Tooltip FR9 describes for button triggers, but a few gaps need fixing first.
- A pin inside a modal
Dialogis announced withannounceMessageinto the page-body live region, which the modal hides, so announce throughuseAnnounceoruseAnnounceRenderedTextto getspec:AST-086FR14 routing. - A labelled HoverCard on a non-button trigger loses
aria-controls/aria-expandedunderpressTrigger="toggle"(HoverCard.tsx:380), which also makes the codemod's span rewrite change behavior; hand that state to the pin only when it wires the trigger, and test it. - The outside-press Effect in
usePressPinduplicatesuseTouchTrigger's outside dismissal; please extend that owner instead. - The hook docs promise the press stops at the trigger (it does not without the component wrapper),
touchTrigger="tap"has no@deprecatedtag, andTooltipPressTrigger/HoverCardPressTriggerare missing from the entry-point exports. - At 2,307 authored lines, please split the DEP-0016 codemod and deprecation into a follow-up in the same release.
[Automated review]
| announcedRef.current = true; | ||
| const text = optionsRef.current.announcement()?.trim(); | ||
| if (text != null && text !== '') { | ||
| announceMessage(text, 'polite'); |
There was a problem hiding this comment.
announceMessage always writes to the page-body live region. Inside an open modal Dialog, that region is inert, so the pin is never heard; useAnnounce() routes to the modal's own region (spec:AST-086 FR14). The shared useAnnounceRenderedText already reads a layer's rendered text and announces it through useAnnounce, so gating it on the pin would replace this Effect.
|
|
||
| // Under the toggle, the press pin owns aria-expanded (true only while | ||
| // pinned) and aria-controls (FR10); the popup role stays here. | ||
| const ownsToggleState = pressTrigger !== 'toggle'; |
There was a problem hiding this comment.
This hands aria-controls/aria-expanded to the press pin for every trigger, but usePressPin wires only button triggers. A labelled HoverCard on a span or link trigger therefore loses HoverCard FR3's popup state, and the codemod rewrites such spans. Something like pressTrigger === 'toggle' && isPressPinTrigger(firstChild) keeps FR3 for the rest.
| // While pinned, a press or focus outside the trigger and the layer closes it. | ||
| useEffect(() => { | ||
| if (!isToggleMode || trigger == null || !isExpanded) { | ||
| return; | ||
| } | ||
| const ownerDocument = trigger.ownerDocument; | ||
| const isInside = (target: EventTarget | null) => { | ||
| if (!(target instanceof Node)) { | ||
| return false; | ||
| } | ||
| const surface = ownerDocument.getElementById(layerId); | ||
| return ( | ||
| trigger.contains(target) || | ||
| (surface != null && surface.contains(target)) | ||
| ); | ||
| }; | ||
| const onPointerDown = (event: PointerEvent) => { | ||
| if (!isInside(event.target)) { | ||
| close(); | ||
| } | ||
| }; | ||
| const onFocusIn = (event: FocusEvent) => { | ||
| if (!isInside(event.target)) { | ||
| close(); | ||
| } | ||
| }; | ||
| ownerDocument.addEventListener('pointerdown', onPointerDown, true); | ||
| ownerDocument.addEventListener('focusin', onFocusIn, true); | ||
| return () => { | ||
| ownerDocument.removeEventListener('pointerdown', onPointerDown, true); | ||
| ownerDocument.removeEventListener('focusin', onFocusIn, true); | ||
| }; | ||
| }, [isToggleMode, trigger, isExpanded, layerId, close]); |
There was a problem hiding this comment.
useTouchTrigger (armOutsideDismiss), which both hooks already call, owns outside dismissal for hover layers, armed from the opening handler. This Effect adds a second path with its own containment test. Could the pin arm that owner from toggle, and could focus leaving close a pinned layer through the existing focus-leave path?
| {name: 'focusTrigger', type: "'auto' | 'always' | 'never'", description: 'When focus should open the tooltip. auto only attaches focus listeners to naturally focusable elements.', default: "'auto'"}, | ||
| {name: 'touchTrigger', type: "'auto' | 'tap' | 'none'", description: 'What a tap does where there is no hover. auto opens on tap unless the trigger performs an action of its own; tap always opens (an info icon rendered as a button); none never opens on touch.', default: "'auto'"}, | ||
| {name: 'touchTrigger', type: "'auto' | 'tap' | 'none'", description: 'What a tap does where there is no hover. auto opens on tap unless the trigger performs an action of its own; tap always opens (deprecated, DEP-0016: use pressTrigger="toggle"); none never opens on touch. Ignored under pressTrigger="toggle".', default: "'auto'"}, | ||
| {name: 'pressTrigger', type: "'none' | 'toggle'", description: 'Whether a press on the trigger pins the tooltip open. none leaves taps to touchTrigger; toggle, on a button trigger, pins on a click, a tap, Enter, or Space and closes on the same press, Escape, a press outside, or focus leaving. The trigger carries aria-expanded and aria-controls, the tooltip is announced once when it pins, and the press stops at the trigger.', default: "'none'"}, |
There was a problem hiding this comment.
"The press stops at the trigger" holds only for the Tooltip/HoverCard components, whose wrapper calls stopPropagation. With the hook alone, a button inside <div onClick> pins the tooltip and also fires the row's handler. The same wording is in useHoverCard.doc.mjs and both hooks' JSDoc.
| * as a button wants: it looks like an action to the DOM, but revealing the | ||
| * tooltip is the only thing it does | ||
| * - `tap`: tap always opens the tooltip, even on a trigger that acts. | ||
| * Deprecated (DEP-0016): pass `pressTrigger="toggle"` instead; `astryx |
There was a problem hiding this comment.
This is prose only. DEP-0016's warning field and spec:AST-017 FR29a ask for static @deprecated metadata on tap, and the description says it is present, but no @deprecated tag exists for it.
| export type {TooltipFocusTrigger, TooltipTouchTrigger} from './useTooltip'; | ||
| export type { | ||
| TooltipFocusTrigger, | ||
| TooltipPressTrigger, |
There was a problem hiding this comment.
TooltipPressTrigger (and HoverCardPressTrigger) is not added to Tooltip/index.ts, HoverCard/index.ts or Layer/index.ts. Those files export the FocusTrigger/TouchTrigger siblings, so the new types are unreachable from @astryxdesign/core (architecture:public-component-api INV1).
a5250c6 to
c543d57
Compare
|
Per the owner's ruling, DEP-0016 (the |
There was a problem hiding this comment.
Changes requested: the toggle now pins and closes as component:Tooltip FR9 describes and the earlier findings are fixed, but a few gaps remain.
- In Chromium, a press on a pinned HoverCard's (or Tooltip's) own text closes it, because the focus-leave check treats focus moving to the page body as leaving (
usePressPin.ts:353-359), while HoverCard FR9 says such a press keeps the pin. - The pin reads
textContentand announces it from its own Effect, butarchitecture:react-component-runtimeINV10 puts that inuseAnnounceRenderedText, which also keeps an unlabelled card's paragraphs apart (today it says "Ada LovelaceAnalyst, Payments"). - The HoverCard verification row cites Enter, Space, press-outside, focus-leave, pointer-leave and unlabelled cases that no HoverCard test covers yet.
- Smaller: the
touchDismissRefbridge Effect and the trigger element kept in state inusePressPin, the duplicatedstopPressPinClick, and the changeset's forward-looking DEP-0016 sentence.
[Automated review]
| if ( | ||
| next instanceof Node && | ||
| (trigger?.contains(next) === true || surface?.contains(next) === true) | ||
| ) { | ||
| return true; | ||
| } | ||
| close(); |
There was a problem hiding this comment.
When the person presses non-focusable text inside the pinned layer, Chromium moves focus from the trigger to the body, so relatedTarget is null and this closes the pin. A pinned HoverCard then closes on a press on its own content, against HoverCard FR9 ("a press on its own content keeps it pinned"), and a pinned Tooltip closes the same way. Could a focus move caused by a press inside the trigger or the layer be left to useTouchTrigger's outside dismissal, with this closing only when focus lands outside both?
| useEffect(() => { | ||
| if (!isToggleMode || trigger == null || !isExpanded || !wiresState) { | ||
| announcedRef.current = false; | ||
| return; | ||
| } | ||
| if (announcedRef.current) { | ||
| return; | ||
| } | ||
| announcedRef.current = true; | ||
| const text = optionsRef.current.announcement()?.trim(); | ||
| if (text != null && text !== '') { | ||
| announce(text, 'polite'); | ||
| } | ||
| }, [isToggleMode, trigger, isExpanded, wiresState, announce]); |
There was a problem hiding this comment.
This Effect announces text the hosts read with textContent. architecture:react-component-runtime INV10 keeps reading and announcing a component's rendered text in hooks/useAnnounceRenderedText.ts, which skips aria-hidden content and keeps blocks apart; an unlabelled HoverCard with two paragraphs is announced here as "Ada LovelaceAnalyst, Payments".
| }, []); | ||
|
|
||
| // The wired trigger, as state: its ARIA follows the pin through effects. | ||
| const [trigger, setTrigger] = useState<HTMLElement | null>(null); |
There was a problem hiding this comment.
architecture:react-component-runtime INV1: DOM nodes must not be stored in React state. The trigger can live in triggerRef alone, with attach/detach setting and restoring its aria-expanded/aria-controls, which would also replace the restore Effect below.
| useEffect(() => { | ||
| touchDismissRef.current = {arm: armOutsideDismiss, disarm: clearTapOpen}; | ||
| }, [armOutsideDismiss, clearTapOpen]); |
There was a problem hiding this comment.
This Effect only copies useTouchTrigger's callbacks into a ref, so it synchronizes no external system (INV3), and the same bridge and hideFromTouch are copied in useHoverCard.tsx:450-452. Letting usePressPin own the touch-trigger wiring (or useTouchTrigger take the pin's close) would leave each hook one call.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [feat] Tooltip and HoverCard (and `useTooltip`, `useHoverCard`) take `pressTrigger` (types `TooltipPressTrigger`, `HoverCardPressTrigger`): `none` (the default, unchanged) or `toggle`. With `toggle` on a button trigger, a click, a tap, Enter, or Space pins the layer open and the same press closes it, while hover and focus still preview it. A pinned layer stays until Escape, a second press, a press outside, or focus leaving, and it takes Escape before the layer beneath it. The press stops at its trigger, so a clickable row, card, or label around it does nothing, and the trigger's own handlers still run. The trigger reports `aria-expanded` and `aria-controls`, and the layer is announced once when it pins, inside a modal too (`component:Tooltip` FR9 and FR10, `component:HoverCard` FR9 and FR10). A trigger that is not a button gets a development warning and keeps tap-only opening; beside `pressTrigger="toggle"`, `touchTrigger` is ignored. `touchTrigger="tap"` is deprecated in favor of `pressTrigger="toggle"` in the next patch (DEP-0016). |
There was a problem hiding this comment.
The last sentence announces the DEP-0016 deprecation, which now ships in the stacked follow-up. Could it move to that follow-up's changeset so this release note describes only what this change does?
c543d57 to
90ae917
Compare
|
Fixed in 90ae917, per the spec owners' ruling:
Kept, per the spec owners' ruling: the changeset says |
There was a problem hiding this comment.
Changes requested: the toggle now behaves as component:Tooltip and component:HoverCard FR9 and FR10 describe, and the earlier findings are fixed, but three items remain.
- The touch-trigger bridge (
touchDismissRef,hideFromTouch, theusePressPincall and thetouchTriggeroverride) is copied line for line intouseTooltip.tsx:375-439anduseHoverCard.tsx:384-448; moving it intousePressPin, so each hook makes one call, keeps the two layers from drifting apart. - At 2,218 authored lines the change is over the 2,000-line threshold; the HoverCard wiring (about 600 lines) could land as a follow-up, or the description could show why the two are inseparable.
- The changeset's last sentence announces DEP-0016, which ships in #9246, so it belongs in that pull request's changeset.
[Automated review]
| // A press pins the card under `pressTrigger="toggle"` (FR9, FR10). The pin | ||
| // arms and drops the touch trigger's outside dismissal, the one owner of a | ||
| // hover layer's dismissal by a press outside. That hook takes the pin's | ||
| // close as part of its hide, so the pin reaches it through a ref, assigned | ||
| // during render below and read only from handlers. | ||
| const touchDismissRef = useRef<{arm: () => void; disarm: () => void} | null>( | ||
| null, | ||
| ); | ||
| const armTouchDismiss = useCallback(() => touchDismissRef.current?.arm(), []); | ||
| const disarmTouchDismiss = useCallback( | ||
| () => touchDismissRef.current?.disarm(), | ||
| [], | ||
| ); | ||
|
|
||
| const { | ||
| isToggleTrigger, | ||
| isPinnedRef: isPressPinnedRef, | ||
| attach: attachPressPin, | ||
| unpin: unpinPress, | ||
| closeIfPinned: closePressPin, | ||
| handleFocusLeave: handlePressFocusLeave, | ||
| isPinShown, | ||
| } = usePressPin({ | ||
| pressTrigger, | ||
| isEnabled, | ||
| isOpen, | ||
| isLayerOpen, | ||
| layerId, | ||
| component: 'HoverCard', | ||
| show: showNow, | ||
| hide: hideNow, | ||
| report: next => (next ? onShow?.() : onHide?.()), | ||
| label, | ||
| armOutsideDismiss: armTouchDismiss, | ||
| disarmOutsideDismiss: disarmTouchDismiss, | ||
| }); | ||
|
|
||
| // An outside press or scroll closes a pinned layer through the pin, so the | ||
| // pin drops with it; otherwise it hides as a tap-open does. | ||
| const hideFromTouch = useCallback(() => { | ||
| if (closePressPin()) { | ||
| return; | ||
| } | ||
| hideNow(); | ||
| }, [closePressPin, hideNow]); | ||
|
|
||
| const { | ||
| isTouchPointerRef, | ||
| isTouchInteraction, | ||
| handlePointerEnter, | ||
| handlePointerDown: handleTouchPointerDown, | ||
| clearTapOpen, | ||
| } = useTouchTrigger({ | ||
| touchTrigger, | ||
| armOutsideDismiss, | ||
| } = useTouchTriggerInternal({ | ||
| // Under the toggle, a trigger no press can pin keeps tap-only opening. | ||
| touchTrigger: pressTrigger === 'toggle' ? 'tap' : touchTrigger, | ||
| isEnabled, | ||
| isControlled: isOpen !== undefined, | ||
| isOpen: isLayerOpen, | ||
| layerId: layerId, | ||
| triggerRef, | ||
| show: showNow, | ||
| hide: hideNow, | ||
| hide: hideFromTouch, | ||
| }); | ||
| touchDismissRef.current = {arm: armOutsideDismiss, disarm: clearTapOpen}; |
There was a problem hiding this comment.
This block is the same as useTooltip.tsx:375-439 apart from the component name and one option: the touchDismissRef bridge, hideFromTouch, the usePressPin call and the touchTrigger override. A fix to how a pin closes on an outside press would then have to land twice. Could usePressPin (or one Layer hook) call useTouchTriggerInternal itself and hand back the touch handles, so each hook makes one call?
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [feat] Tooltip and HoverCard (and `useTooltip`, `useHoverCard`) take `pressTrigger` (types `TooltipPressTrigger`, `HoverCardPressTrigger`): `none` (the default, unchanged) or `toggle`. With `toggle` on a button trigger, a click, a tap, Enter, or Space pins the layer open and the same press closes it, while hover and focus still preview it. A pinned layer stays until Escape, a second press, a press outside, or focus leaving, and it takes Escape before the layer beneath it. The press stops at its trigger, so a clickable row, card, or label around it does nothing, and the trigger's own handlers still run. The trigger reports `aria-expanded` and `aria-controls`, and the layer is announced once when it pins, inside a modal too (`component:Tooltip` FR9 and FR10, `component:HoverCard` FR9 and FR10). A trigger that is not a button gets a development warning and keeps tap-only opening; beside `pressTrigger="toggle"`, `touchTrigger` is ignored. `touchTrigger="tap"` is deprecated in favor of `pressTrigger="toggle"` in the next patch (DEP-0016). |
90ae917 to
40c79f0
Compare
|
Fixed in 40c79f0:
|
There was a problem hiding this comment.
Changes requested: the toggle behaves as component:Tooltip and component:HoverCard FR9 and FR10 describe, and the earlier touch-bridge and changeset points are fixed, but three items remain.
- Switching
pressTriggerfromtoggletononewhile the layer is pinned leaves the pin set, so that tooltip or card never closes on pointer or focus leave again (usePressPin.ts:421-425with each host'sscheduleHide); havingscheduleHideask the pin's ownisHeldOpen(), plus a rerender test that leaves after the switch, would cover it. - At 2,348 authored lines the change is over the 2,000-line threshold, and the HoverCard wiring (about 580 lines under HoverCard FR9, FR10 and AR3) could land as a stacked pull request ahead of #9246.
- The unmount Effect at
usePressPin.ts:535repeats the cleanupattach(null)already runs on every host and names no external system, so it can be removed.
[Automated review]
| unwire(); | ||
| const current = optionsRef.current; | ||
| if (element == null || current.pressTrigger !== 'toggle') { | ||
| return; | ||
| } |
There was a problem hiding this comment.
When pressTrigger turns from toggle to none while the layer is pinned, this unwires the trigger but leaves isPinnedRef true, and scheduleHide in both hosts (useTooltip.tsx:437, useHoverCard.tsx:437) still returns early on it. The tooltip or card then never closes on pointer or focus leave again, even after a trigger press hides it and a new hover preview opens it, until Escape (reproduced in jsdom for both). Could scheduleHide ask the pin's own isHeldOpen() (already false once pressTrigger is not toggle), or could the pin drop when the toggle unwires?
| writeAttribute(trigger, 'aria-controls', isLayerOpen ? layerId : null); | ||
| }); | ||
|
|
||
| useEffect(() => unwire, [unwire]); |
There was a problem hiding this comment.
This cleanup repeats what each host's callback ref already does on detach: attach(null) runs wire(null), which unwires (useTooltip.tsx:592, useHoverCard.tsx:630, and the wrappers' ref(null) cleanups in Tooltip.tsx and HoverCard.tsx). It also names no external system (architecture:react-component-runtime INV3, INV5). Could it be removed?
40c79f0 to
fc05aff
Compare
|
Fixed in fc05aff:
|
There was a problem hiding this comment.
Changes requested: pressTrigger="toggle" now behaves as component:Tooltip FR9 and FR10 describe for a child trigger, and the earlier stale-pin, unmount-Effect and size points are fixed, but two FR9 gaps remain. Please fix both and add a test for each.
- With
anchorRef, the pinning click still reaches a plainonClickrow around the anchored button, because only the child wrapper stops it (Tooltip.tsx:263-312). - With
focusTrigger="never", focus moving to another control never closes a pinned tooltip, because the close rides on focus listeners that option skips (useTooltip.tsx:581-588); the pin listening forfocusouton the trigger it wires would cover it.
[Automated review]
| <div ref={wrapperRef} {...stylex.props(styles.wrapperContents)}> | ||
| <div | ||
| ref={wrapperRef} | ||
| onClick={stopPressPinClick} |
There was a problem hiding this comment.
Only this wrapper (and the inline span's) stops a pinning press. The anchorRef path above (lines 263-312) wires an external element with no wrapper, so with pressTrigger="toggle" a click on an anchored button inside a plain <div onClick> both pins the tooltip and fires the row (reproduced in jsdom: one row call; zero with a child trigger). FR9 says the press "does not reach a clickable row, card, or container around it". Stop the marked click for anchorRef triggers too, or record the case as an FR9 gap beside the hook caveat, with a test.
| const handleFocusOut = useCallback( | ||
| (event: FocusEvent) => { | ||
| // A pinned tooltip closes when focus leaves the trigger and the layer. | ||
| if (handlePressFocusLeave(event)) { |
There was a problem hiding this comment.
This is the pin's only focus-leave close, and handleFocusOut is attached only when focusTrigger is always or auto on a focusable trigger (lines 581-588). Under focusTrigger="never", a pinned tooltip stays open with aria-expanded="true" after Tab moves focus away (reproduced in jsdom), against FR9's "focus moving outside them closes it". Having usePressPin listen for focusout on the trigger it wires, whatever focusTrigger is, would cover it; please add a focusTrigger="never" case.
`component:Tooltip` FR9 and FR10, `spec:AST-003` FR4: - `pressTrigger` (`none`, the default, or `toggle`) on Tooltip and useTooltip, with the type TooltipPressTrigger. With `toggle` on a button trigger, a click, a tap, Enter, or Space pins the tooltip and the same press closes it; hover and focus still preview it. - A pinned tooltip stays until Escape, a second press, a press or scroll outside (the touch trigger's outside dismissal, which the pin arms: the pin owns the layer's touch trigger, so useTooltip makes one usePressPin call), or focus moving onto an element outside the trigger and the tooltip (the pin listens for it on the trigger, whatever `focusTrigger` is); a press on the tooltip's own text keeps it. It takes Escape before the layer beneath it. A `pressTrigger` that changes after mount wires or unwires the trigger, and a tooltip whose `pressTrigger` turns to `none` while pinned closes on leave again. - The press stops at its trigger. With `anchorRef`, the caller's own handlers still receive the press: the anchor's `onClick` runs, and a plain `onClick` element around the anchor receives it unless the anchor's `onClick` stops it; Astryx clickable rows, cards, and containers ignore it (FR9). The trigger carries aria-expanded and aria-controls on a button trigger, and the tooltip's rendered text is announced once when it pins, through useAnnounceRenderedText (`spec:AST-086` FR12). - A non-button trigger gets one development warning and keeps tap-only opening; a trigger whose tooltip repeats its own text pins without toggle state (`Layer/usePressPin.ts`, the hover layers' shared press pin). - LayerPressTrigger and the touch trigger's armOutsideDismiss (useTouchTriggerInternal) stay internal: neither is exported from the Layer entry point, and the public useTouchTrigger return is unchanged.
fc05aff to
26d7af9
Compare
|
Fixed in 26d7af9, per the maintainers' ruling:
HoverCard has no |
Status: green at 26d7af9 (on main a55352c): every gate under Testing finished at this head.
User need
A tooltip that is the only home of its information (an info icon, a "What is this?" button) can't be kept open by touch, keyboard or screen-reader users. Hover opens it and leaving closes it.
touchTrigger="tap"opens it on a tap, but a mouse click, Enter or Space doesn't keep it, and nothing pins it. So the information is a hover-only path, whicharchitecture:interaction-modalityrules out for content that has no other home.Problem and solution fit
pressTrigger="toggle"makes the trigger a toggle. On a button trigger, a click, a tap, Enter or Space pins the tooltip, and the same press closes it. Hover and focus still preview it. That gives every modality one way to keep the tooltip.spec:AST-003FR4, DEC-6).Current specification
component:TooltipFR9, FR10, AR6 and DEC-1;spec:AST-003FR4 and DEC-6. All are current at 1d8696a (#9175).Public delta and caller burden
pressTrigger: 'none' | 'toggle'onTooltipanduseTooltip, with the typeTooltipPressTrigger. The defaultnonechanges nothing.toggleon a button trigger (a<button>orrole="button"), a click, a tap, Enter or Space pins the tooltip, and the same press closes it.focusTriggeris.onOpenChangeand doesn't change itself.aria-expanded(true only while pinned) andaria-controlswhile the tooltip shows. When it pins, the tooltip's rendered text is announced once by the shared rendered-text announcer (spec:AST-086FR12), which keeps blocks apart and leaves hidden content out. It reaches a polite live region throughuseAnnounce, so inside a modal it's heard in the modal's own region (spec:AST-086FR14).<label>around it doesn't operate its control. The trigger's own handlers still run. WithanchorRef, the caller's own handlers still receive the press: the anchor'sonClickruns, and a plainonClickelement around the anchor receives it unless the anchor'sonClickstops it; Astryx clickable rows, cards and containers ignore it (FR9, by the spec owners' ruling: a tap that already acts stays with its control, DEC-1). With the bare hook, Astryx clickable rows and cards ignore the pinning press, but a plainonClickancestor still receives it; the component stops it.pressTriggerthat changes after the trigger mounted wires or unwires it then, and a tooltip whosepressTriggerturns tononewhile pinned closes on leave again.pressTrigger="toggle",touchTriggeris ignored.useTooltipInternal({repeatsTriggerText: true})), not public API.Before and after:
Not in this pull request:
pressTrigger(component:HoverCardFR9, FR10, AR3), on the same shared press pin: the stacked HoverCard pull request (branchfeat/hover-card-press-trigger).touchTrigger="tap": feat(Tooltip, HoverCard, cli): deprecate touchTrigger="tap" (DEP-0016), with its upgrade codemod #9246, stacked on the HoverCard pull request. All three ship in the same release.touchTrigger="tap": its move to InfoTip is FieldLabel's own change (component:FieldLabelFR9).autowalk (component:TooltipFR5, DEC-1), which ships with CLN-0024 at the next minor.API changes
Added public rows, decided by
component:TooltipFR9 and FR10 (#9175). They go to human review underarchitecture:public-component-apiINV25.@astryxdesign/coreTooltipProps.pressTrigger?: TooltipPressTrigger(default'none')<Tooltip content="…" pressTrigger="toggle"><Button label="What is this?">?</Button></Tooltip>@astryxdesign/coreTooltipOptions.pressTrigger?: TooltipPressTriggeruseTooltip({pressTrigger: 'toggle'})@astryxdesign/coretype TooltipPressTrigger = 'none' | 'toggle'const mode: TooltipPressTrigger = 'toggle';Design and alternatives
Layer/usePressPin.ts. It owns the layer's touch trigger (useTouchTriggerInternal), souseTooltipmakes oneusePressPincall (architecture:react-component-runtimeINV16).isHeldOpen(), and a pin counts only while its layer shows.useClickableContainerignores a marked click.LayerPressTriggertype and the touch trigger'sarmOutsideDismiss(onuseTouchTriggerInternal) are not exported from theLayerentry point; the publicuseTouchTriggerreturn is unchanged.aria-expandedandaria-controlswhile it wires a button trigger, and restores the trigger's own values when it unwires. The trigger element itself stays in a ref, never in React state.touchTriggerin a patch, which is CLN-0024 at the next minor.Evidence
layerDismissalFamilies.test.tsxfails.useAnnounce(inside a modal Dialog too), and a scroll that starts outside closing a pin.pressTriggerthat turns totoggleafter mount fail, in jsdom and, for the press, in Chromium (3 of 3 runs).usePressPin.pressTriggerturns tononenever closes on leave, and a non-button trigger warns four times across a mount and two re-renders.focusTrigger="never"tooltip stays pinned when focus moves away: in jsdom, and in Chromium (3 of 3 runs).anchorRefcases state FR9's anchored clause, which fc05aff already meets. A stop at the anchored trigger itself fails them: the action anchor'sonClickand the plain row lose the press (2 of 5), and the Chromium case fails (3 of 3 runs).Layer/usePressPin.test.tsx(31 tests):focusTrigger="never";pressTriggerturning totoggleafter mount, and tononewhile pinned.aria-expanded,aria-controls, restore on cleanup, one announcement, including inside a modal Dialog.<label>, plus the released behavior without the toggle.anchorRef: the action anchor's ownonClickfires; an Astryx ClickableCard, Item row and click-expanding Table row don't activate; a plain row receives the press unless the anchor'sonClickstops it.layerDismissalFamilies.test.tsx: a pinned Tooltip over a Dialog takes Escape first, and the Dialog stays open.Layer/usePressPin.calls.test.tsx: the host makes oneusePressPincall per render, and every touch-trigger call comes from inside it.hooks/useAnnounceRenderedText.adopters.test.tsx: a Tooltip pinned by a press hands its text to the shared rendered-text announcer and announces none of it itself; the registry check listsuseTooltipas an adopter (architecture:react-component-runtimeINV10).TouchTrigger.a11y.chromium.spec.ts: real-touch and keyboard cases on the newa11y/Tooltip press togglestories: a tap pins and the next tap closes; a tap outside closes; Enter pins and Space closes; inside a ClickableCard the tap pins and the card doesn't open. Under a mouse: a click on a pinned tooltip's own text, which moves focus to the body, keeps the pin; a click on an anchored trigger runs its own click handler and pins it, and the ClickableCard around it ignores the press; withfocusTrigger="never", Tab to another control closes a pin.anchorRef, by the spec owners' ruling: the caller's own handlers still receive the press (the anchor'sonClickruns; a plainonClickelement around the anchor receives it unless thatonClickstops it; Astryx clickable rows, cards and containers ignore it). Its verification row names the anchored cases. FR9 keeps a gap note for its own-text clause, which no component reaches yet (only an internal option marks such a trigger), with its current-state item; its verification rows cite the tests above (added toverified_by). Its current state keeps one item, "DEP-0016 is not built", which feat(Tooltip, HoverCard, cli): deprecate touchTrigger="tap" (DEP-0016), with its upgrade codemod #9246 removes.Tooltip.tsx(trigger spans).Split
The HoverCard wiring (about 580 lines, under
component:HoverCardFR9, FR10 and AR3) is its own pull request, stacked between this one and #9246, by the maintainers' ruling. All three ship in the same release. This one is 16 files, +2,008/−66: the shared press pin and touch-trigger wiring, Tooltip's wiring, its tests and story, its record and docs, and its changeset.Scope
Writing rubric results
The rubric in
docs/contributing/spec-writing.md, over the changed parts ofTooltip.spec.md:Testing
Local gates at 26d7af9 (on main a55352c):
pnpm check:repo,pnpm check:knowledge,pnpm check:changesets,pnpm typecheck(0 errors),pnpm lint(0 errors), CI-strict ESLint on the changed files, the private-attribute baseline check, the root build, and the Storybook build..doc.mjsfiles, which are unformatted on main too and outside the repository's formatting scope.pnpm test:ui: 17,426 tests pass.pnpm test:node: 15,102 pass. Its 2 failures, in the releasecache.test.mjs, are independent of this change (fix(cli): a crashed run's release install is recognized by the Node it ran on, whatever its name #8520).TouchTrigger.a11y.chromium.spec.tspasses in Chromium, twice (38 of 38), the three mouse cases included..a11y.browser.spec.tsspecs pass ten times each on one worker with 4 CPUs: 220 of 220.