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 |
|---|---|---|---|
| Tokenizer | 85.4 kB (+627 B) · 18.1 kB CSS (0) | — | — · — · — |
| DateTimeInput | 195.9 kB (+549 B) · 18.1 kB CSS (0) | — | — · — · — |
| DateInput | 173.4 kB (+524 B) · 16.4 kB CSS (0) | — | — · — · — |
134 more components
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| PowerSearch | 271.7 kB (+516 B) · 23.1 kB CSS (0) | — | — · — · — |
| ChatComposer | 68.8 kB (+509 B) · 11.2 kB CSS (0) | — | — · — · — |
| Typeahead | 74.9 kB (+509 B) · 12.1 kB CSS (0) | — | — · — · — |
| ChatComposerTokenElement | 66.9 kB (+505 B) · 10.6 kB CSS (0) | — | — · — · — |
| ChatComposerInput | 66.9 kB (+504 B) · 10.6 kB CSS (0) | — | — · — · — |
| BaseTypeahead | 62.5 kB (+493 B) · 10.3 kB CSS (0) | — | — · — · — |
| Lightbox | 103.2 kB (+177 B) · 9.8 kB CSS (0) | — | — · — · — |
| TimeInput | 149.9 kB (+158 B) · 15.5 kB CSS (0) | — | — · — · — |
| Slider | 102.0 kB (+145 B) · 8.7 kB CSS (0) | — | — · — · — |
| AlertDialog | 95.9 kB (+135 B) · 13.7 kB CSS (0) | — | — · — · — |
| Toast | 89.4 kB (+115 B) · 9.6 kB CSS (0) | — | — · — · — |
| Switch | 96.4 kB (+108 B) · 9.8 kB CSS (0) | — | — · — · — |
| SegmentedControl | 89.6 kB (+41 B) · 6.8 kB CSS (0) | — | — · — · — |
| ButtonGroup | 40.1 kB (+37 B) · 3.7 kB CSS (0) | — | — · — · — |
| Drawer | 44.8 kB (+18 B) · 8.2 kB CSS (0) | — | — · — · — |
| ContextMenu | 90.5 kB (+14 B) · 16.6 kB CSS (0) | — | — · — · — |
| SideNav | 81.2 kB (+11 B) · 10.7 kB CSS (0) | — | — · — · — |
| TopNav | 63.3 kB (+9 B) · 10.2 kB CSS (0) | — | — · — · — |
| TopNavHeading | 66.3 kB (+9 B) · 10.4 kB CSS (0) | — | — · — · — |
| AvatarGroupOverflow | 44.5 kB (+8 B) · 5.5 kB CSS (0) | — | — · — · — |
| ListItem | 65.0 kB (+8 B) · 10.9 kB CSS (0) | — | — · — · — |
| AppShell | 81.5 kB (+7 B) · 12.8 kB CSS (0) | — | — · — · — |
| FileInput | 64.2 kB (+7 B) · 10.6 kB CSS (0) | — | — · — · — |
| SideNavHeading | 66.8 kB (+7 B) · 10.6 kB CSS (0) | — | — · — · — |
| TopNavMegaMenu | 61.1 kB (+7 B) · 11.2 kB CSS (0) | — | — · — · — |
| BreadcrumbItem | 76.0 kB (+6 B) · 11.5 kB CSS (0) | — | — · — · — |
| ComplexSelector | 65.9 kB (+6 B) · 11.3 kB CSS (0) | — | — · — · — |
| Link | 48.8 kB (+6 B) · 6.9 kB CSS (0) | — | — · — · — |
| ChatDictationButton | 52.3 kB (+5 B) · 9.4 kB CSS (0) | — | — · — · — |
| CheckboxInput | 59.9 kB (+5 B) · 9.1 kB CSS (0) | — | — · — · — |
| CheckboxListItem | 66.0 kB (+5 B) · 10.9 kB CSS (0) | — | — · — · — |
| InfoTip | 43.3 kB (+5 B) · 5.3 kB CSS (0) | — | — · — · — |
| SideNavItem | 61.7 kB (+5 B) · 10.4 kB CSS (0) | — | — · — · — |
| DropdownMenuSubMenu | 64.7 kB (+4 B) · 9.4 kB CSS (0) | — | — · — · — |
| MobileNav | 61.6 kB (+4 B) · 9.9 kB CSS (0) | — | — · — · — |
| TabMenu | 63.2 kB (+4 B) · 10.4 kB CSS (0) | — | — · — · — |
| TextInput | 62.8 kB (+4 B) · 10.8 kB CSS (0) | — | — · — · — |
| Thumbnail | 56.1 kB (+4 B) · 10.9 kB CSS (0) | — | — · — · — |
| ChatLayout | 55.0 kB (+3 B) · 9.8 kB CSS (0) | — | — · — · — |
| ChatSendButton | 52.2 kB (+3 B) · 9.3 kB CSS (0) | — | — · — · — |
| NumberInput | 66.9 kB (+3 B) · 11.1 kB CSS (0) | — | — · — · — |
| Step | 29.8 kB (+3 B) · 5.6 kB CSS (0) | — | — · — · — |
| Stepper | 53.4 kB (+3 B) · 9.5 kB CSS (0) | — | — · — · — |
| TableSelectionToolbar | 61.3 kB (+3 B) · 12.4 kB CSS (0) | — | — · — · — |
| Avatar | 39.5 kB (+2 B) · 5.5 kB CSS (0) | — | — · — · — |
| Banner | 53.7 kB (+2 B) · 9.8 kB CSS (0) | — | — · — · — |
| Calendar | 62.7 kB (+2 B) · 9.9 kB CSS (0) | — | — · — · — |
| MobileNavToggle | 52.1 kB (+2 B) · 9.3 kB CSS (0) | — | — · — · — |
| ToggleButton | 52.6 kB (+2 B) · 9.4 kB CSS (0) | — | — · — · — |
| TopNavMenu | 66.0 kB (+2 B) · 10.5 kB CSS (0) | — | — · — · — |
| ChatLayoutScrollButton | 52.2 kB (+1 B) · 9.5 kB CSS (0) | — | — · — · — |
| ChatMessageList | 23.9 kB (+1 B) · 6.9 kB CSS (0) | — | — · — · — |
| CommandPalette | 59.5 kB (+1 B) · 12.4 kB CSS (0) | — | — · — · — |
| DrawerHeader | 54.7 kB (+1 B) · 12.2 kB CSS (0) | — | — · — · — |
| IconButton | 51.7 kB (+1 B) · 9.3 kB CSS (0) | — | — · — · — |
| SelectorOption | 19.5 kB (+1 B) · 5.8 kB CSS (0) | — | — · — · — |
| BottomSheet | 58.6 kB (0) · 7.5 kB CSS (0) | — | — · — · — |
| BottomSheetSwitcher | 17.5 kB (0) · 3.4 kB CSS (0) | — | — · — · — |
| Carousel | 59.6 kB (0) · 10.3 kB CSS (0) | — | — · — · — |
| ChatToolCalls | 27.8 kB (0) · 8.3 kB CSS (0) | — | — · — · — |
| ClickableCard | 16.7 kB (0) · 7.1 kB CSS (0) | — | — · — · — |
| Collapsible | 24.8 kB (0) · 4.5 kB CSS (0) | — | — · — · — |
| CommandPaletteItem | 11.9 kB (0) · 3.6 kB CSS (0) | — | — · — · — |
| ContextMenuItem | 26.8 kB (0) · 6.1 kB CSS (0) | — | — · — · — |
| DropdownMenuCheckboxItem | 21.3 kB (0) · 6.4 kB CSS (0) | — | — · — · — |
| DropdownMenuItem | 20.5 kB (0) · 6.0 kB CSS (0) | — | — · — · — |
| DropdownMenuRadioItem | 21.3 kB (0) · 6.4 kB CSS (0) | — | — · — · — |
| FieldLabel | 47.1 kB (0) · 5.6 kB CSS (0) | — | — · — · — |
| HoverCard | 24.4 kB (0) · 4.3 kB CSS (0) | — | — · — · — |
| Item | 16.1 kB (0) · 5.3 kB CSS (0) | — | — · — · — |
| LayerProvider | 4.1 kB (0) · 20 B CSS (0) | — | — · — · — |
| Markdown | 162.2 kB (0) · 19.5 kB CSS (0) | — | — · — · — |
| MoreMenu | 86.3 kB (0) · 16.7 kB CSS (0) | — | — · — · — |
| Outline | 23.7 kB (0) · 4.1 kB CSS (0) | — | — · — · — |
| Pagination | 103.3 kB (0) · 17.3 kB CSS (0) | — | — · — · — |
| ProgressBar | 11.5 kB (0) · 6.2 kB CSS (0) | — | — · — · — |
| RadioListItem | 62.7 kB (0) · 9.8 kB CSS (0) | — | — · — · — |
| SegmentedControlItem | 16.2 kB (0) · 4.3 kB CSS (0) | — | — · — · — |
| Spinner | 20.5 kB (0) · 6.1 kB CSS (0) | — | — · — · — |
| StatusDot | 27.6 kB (0) · 5.3 kB CSS (0) | — | — · — · — |
| Tab | 13.3 kB (0) · 4.2 kB CSS (0) | — | — · — · — |
| Text | 7.6 kB (0) · 5.3 kB CSS (0) | — | — · — · — |
| Timer | 9.7 kB (0) · 5.9 kB CSS (0) | — | — · — · — |
| Timestamp | 23.2 kB (0) · 9.6 kB CSS (0) | — | — · — · — |
| Token | 27.4 kB (0) · 4.7 kB CSS (0) | — | — · — · — |
| TopNavItem | 12.6 kB (0) · 4.0 kB CSS (0) | — | — · — · — |
| TopNavMegaMenuItem | 12.7 kB (0) · 4.2 kB CSS (0) | — | — · — · — |
| CheckboxList | 54.3 kB (−1 B) · 7.9 kB CSS (0) | — | — · — · — |
| CodeBlock | 66.3 kB (−1 B) · 10.6 kB CSS (0) | — | — · — · — |
| CommandPaletteInput | 27.9 kB (−1 B) · 7.5 kB CSS (0) | — | — · — · — |
| Dialog | 22.5 kB (−1 B) · 6.7 kB CSS (0) | — | — · — · — |
| DialogHeader | 54.5 kB (−1 B) · 10.8 kB CSS (0) | — | — · — · — |
| Heading | 8.2 kB (−1 B) · 5.3 kB CSS (0) | — | — · — · — |
| Selector | 93.2 kB (−1 B) · 16.9 kB CSS (0) | — | — · — · — |
| SideNavCollapseButton | 52.6 kB (−1 B) · 9.4 kB CSS (0) | — | — · — · — |
| Tooltip | 25.9 kB (−1 B) · 4.3 kB CSS (0) | — | — · — · — |
| TreeList | 41.1 kB (−1 B) · 5.4 kB CSS (0) | — | — · — · — |
| DropdownMenu | 86.1 kB (−2 B) · 16.7 kB CSS (0) | — | — · — · — |
| SelectableCard | 16.9 kB (−2 B) · 7.3 kB CSS (0) | — | — · — · — |
| Button | 51.7 kB (−3 B) · 9.3 kB CSS (0) | — | — · — · — |
| Table | 99.9 kB (−3 B) · 17.3 kB CSS (0) | — | — · — · — |
| InputGroup | 52.1 kB (−4 B) · 7.8 kB CSS (0) | — | — · — · — |
| TableHeaderCell | 91.3 kB (−4 B) · 16.8 kB CSS (0) | — | — · — · — |
| AvatarGroup | 42.2 kB (−5 B) · 5.6 kB CSS (0) | — | — · — · — |
| Field | 50.5 kB (−5 B) · 7.2 kB CSS (0) | — | — · — · — |
| TableCell | 91.7 kB (−5 B) · 17.0 kB CSS (0) | — | — · — · — |
| TextArea | 57.9 kB (−5 B) · 9.2 kB CSS (0) | — | — · — · — |
| RadioList | 51.9 kB (−6 B) · 7.2 kB CSS (0) | — | — · — · — |
| Popover | 78.5 kB (−8 B) · 13.4 kB CSS (0) | — | — · — · — |
| MultiSelector | 88.7 kB (−14 B) · 16.3 kB CSS (0) | — | — · — · — |
| DateRangeInput | 85.8 kB (−18 B) · 12.1 kB CSS (0) | — | — · — · — |
| charts/Chart | 176.0 kB (new) · 494 B CSS (new) | — | — · — · — |
| charts/ChartLegend | 35.8 kB (new) · 224 B CSS (new) | — | — · — · — |
| charts/ChartTooltip | 153.4 kB (new) · 423 B CSS (new) | — | — · — · — |
| lab/ChatEmojiPicker | 96.5 kB (new) · 773 B CSS (new) | — | — · — · — |
| lab/ChatReactionBar | 97.7 kB (new) · 1.1 kB CSS (new) | — | — · — · — |
| lab/CommentComposer | 138.2 kB (new) · 1.9 kB CSS (new) | — | — · — · — |
| lab/CommentPin | 52.7 kB (new) · 1.2 kB CSS (new) | — | — · — · — |
| lab/Comments | 307.1 kB (new) · 2.5 kB CSS (new) | — | — · — · — |
| lab/CommentThread | 302.0 kB (new) · 2.4 kB CSS (new) | — | — · — · — |
| lab/DiffViewer | 78.1 kB (new) · 2.1 kB CSS (new) | — | — · — · — |
| lab/EmojiPicker | 81.3 kB (new) · 751 B CSS (new) | — | — · — · — |
| lab/ListInput | 95.4 kB (new) · 1.6 kB CSS (new) | — | — · — · — |
| lab/NavigationScreen | 78.4 kB (new) · 1.5 kB CSS (new) | — | — · — · — |
| lab/NavigationStack | 78.4 kB (new) · 1.5 kB CSS (new) | — | — · — · — |
| lab/PageControl | 71.7 kB (new) · 709 B CSS (new) | — | — · — · — |
| lab/Schedule | 99.4 kB (new) · 3.9 kB CSS (new) | — | — · — · — |
| lab/SelectableCardGroup | 59.7 kB (new) · 162 B CSS (new) | — | — · — · — |
| lab/TourStep | 84.7 kB (new) · 590 B CSS (new) | — | — · — · — |
| lab/TransferList | 90.1 kB (new) · 1.7 kB CSS (new) | — | — · — · — |
| lab/TransferListSelector | 97.1 kB (new) · 1.9 kB CSS (new) | — | — · — · — |
| richtext/RichTextEditor | 290.9 kB (new) · 2.6 kB CSS (new) | — | — · — · — |
| richtext/RichTextEditorToolbar | 244.1 kB (new) · 454 B CSS (new) | — | — · — · — |
| richtext/RichTextView | 271.1 kB (new) · 2.3 kB CSS (new) | — | — · — · — |
Package size: measured; main has no measurement of the merge base to compare yet.
API changes: cli +14 · ~5 · −0 (against the merge base's committed report)
Committed API report behind this build: cli 19 rows, core 24 rows; run pnpm api:report.
19 API changes (advisory)
| Package | Where | Signature | Usage | |
|---|---|---|---|---|
| + | cli |
./api · Types · type SearchResultEntry |
deprecated?: ComponentDeprecation | undefined |
import type {SearchResultEntry} from '@astryxdesign/cli/api'; const x: SearchResultEntry = {deprecated: …} |
| + | cli |
./authoring · Types · interface ComponentDeprecation |
interface ComponentDeprecation |
import type {ComponentDeprecation} from '@astryxdesign/cli/authoring' |
| + | cli |
./authoring · Types · interface ComponentIconSlotDoc |
interface ComponentIconSlotDoc |
import type {ComponentIconSlotDoc} from '@astryxdesign/cli/authoring' |
| + | cli |
./authoring · Types · interface ComponentIconSlotStateDoc |
interface ComponentIconSlotStateDoc |
import type {ComponentIconSlotStateDoc} from '@astryxdesign/cli/authoring' |
| + | cli |
./authoring · Types · interface MultiComponentDoc |
deprecated?: ComponentDeprecation |
import type {MultiComponentDoc} from '@astryxdesign/cli/authoring'; const x: MultiComponentDoc = {deprecated: …} |
| + | cli |
./authoring · Types · interface SingleComponentDoc |
deprecated?: ComponentDeprecation |
import type {SingleComponentDoc} from '@astryxdesign/cli/authoring'; const x: SingleComponentDoc = {deprecated: …} |
| + | cli |
./doc · Types · interface ComponentBaseDoc |
deprecated?: ComponentDeprecation |
import type {ComponentBaseDoc} from '@astryxdesign/cli/doc'; const x: ComponentBaseDoc = {deprecated: …} |
| + | cli |
./doc · Types · interface SubComponentDoc |
deprecated?: ComponentDeprecation |
import type {SubComponentDoc} from '@astryxdesign/cli/doc'; const x: SubComponentDoc = {deprecated: …} |
| + | cli |
./doc · Also exported from an earlier entry point · ComponentDeprecation |
ComponentDeprecation (./authoring) |
import {ComponentDeprecation} from '@astryxdesign/cli/doc' |
| + | cli |
./doc · Also exported from an earlier entry point · ComponentIconSlotDoc |
ComponentIconSlotDoc (./authoring) |
import {ComponentIconSlotDoc} from '@astryxdesign/cli/doc' |
| + | cli |
./doc · Also exported from an earlier entry point · ComponentIconSlotStateDoc |
ComponentIconSlotStateDoc (./authoring) |
import {ComponentIconSlotStateDoc} from '@astryxdesign/cli/doc' |
| + | cli |
./template · Also exported from an earlier entry point · ComponentDeprecation |
ComponentDeprecation (./authoring) |
import {ComponentDeprecation} from '@astryxdesign/cli/template' |
| + | cli |
./template · Also exported from an earlier entry point · ComponentIconSlotDoc |
ComponentIconSlotDoc (./authoring) |
import {ComponentIconSlotDoc} from '@astryxdesign/cli/template' |
| + | cli |
./template · Also exported from an earlier entry point · ComponentIconSlotStateDoc |
ComponentIconSlotStateDoc (./authoring) |
import {ComponentIconSlotStateDoc} from '@astryxdesign/cli/template' |
| ~ | cli |
./api · Functions · function search |
function search: (query: string, options?: { cwd?: string | undefined; type?: "template" | "component" | "hook" | "theme" | "doc" | undefined; limit?: number | undefined }) => Promise<SearchResponse> → function search: (query: string, options?: { cwd?: string | undefined; type?: "template" | "component" | "doc" | "theme" | "hook" | undefined; limit?: number | undefined }) => Promise<SearchResponse> |
import {search} from '@astryxdesign/cli/api'; search(…) |
| ~ | cli |
./authoring · Types · interface MultiComponentDoc |
theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } |
import type {MultiComponentDoc} from '@astryxdesign/cli/authoring'; const x: MultiComponentDoc = {theming: …} |
| ~ | cli |
./authoring · Types · interface SingleComponentDoc |
theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } |
import type {SingleComponentDoc} from '@astryxdesign/cli/authoring'; const x: SingleComponentDoc = {theming: …} |
| ~ | cli |
./doc · Types · interface ComponentBaseDoc |
theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } |
import type {ComponentBaseDoc} from '@astryxdesign/cli/doc'; const x: ComponentBaseDoc = {theming: …} |
| ~ | cli |
./doc · Types · interface SubComponentDoc |
theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } |
import type {SubComponentDoc} from '@astryxdesign/cli/doc'; const x: SubComponentDoc = {theming: …} |
Wide change (shared styles: packages/core/src/theme/derivedVarRegistry.test.ts, packages/core/src/theme/derivedVarRegistry.ts, packages/core/src/theme/generateThemeRules.test.ts, and 1 more): add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.
CI impact: +16.9 job-min per update · pool +14% · time to green −10.3 min · standard runners · 5 CI action or script files changed (not measured) ⚠️
| Before | After | Change | |
|---|---|---|---|
| Job-minutes per PR update | 116.8 | 133.7 | +16.9 |
| Runner pool, runner-minutes a day | 53,692 | 61,463 | +7,771 (+14%) |
| Average concurrent jobs | 37.3 | 42.7 | +5.4 |
| Time to green (median → this run) | 28.3 min | 17.9 min | −10.3 min |
| Runs a day | 460 | 460 | 0 |
Changed jobs: pr-interaction changed: standard · pr-interaction-webkit changed: standard
Not measured (the jobs that use these keep their recent minutes above): .github/scripts/ci-interaction-webkit.test.mjs, .github/scripts/ci-interaction.test.mjs, .github/scripts/playwright-legs.mjs, .github/scripts/playwright-legs.test.mjs, .github/scripts/release-publish-workflow.test.mjs.
Flags: runner-pool usage +14%.
Assumptions: before is each job's average over the last 20 pull-request runs; after keeps that for unchanged jobs and uses this run's minutes for added or changed ones, weighted by how often each changed job ran in those runs; runs a day averages the seven whole days before today; time to green compares this run with the median of those runs that ran the same jobs.
Generated by PR Enrichment workflow | View full report
There was a problem hiding this comment.
Requesting changes for one fix in the new root option.
- In a closed shadow root,
rootjudges only where a press starts, so a press that starts outside the layer and lifts on it closes the layer while the same press leaves a light-DOM or open-root layer open; judge the lift at the root as well (for example, registerhandleUponrootand, at the document, skip a lift whose composed path holdsroot.host) and add a test for that press.
Non-blocking: handleCancel does not check pointerTypes when closesOnPan is off, and the onOutside JSDoc and the file header still describe only the lift-and-cancel model. The pan close, its 10 px threshold on either axis, the second-finger handling and the unchanged default watches all check out.
[Automated review]
| const judges: EventTarget[] = | ||
| root != null && root !== doc ? [doc, root] : [doc]; |
There was a problem hiding this comment.
Only pointerdown is judged at root; pointerup (line 129) stays on the document, which sees a closed root's contents as its host. So, with root set, a press that starts outside the layer and lifts on it closes a layer in a closed root but not one in the light DOM or an open root (with and without closesOnPan). Consider registering handleUp on root too and, at the document, skipping a lift whose composed path holds root.host, plus a test for that press.
There was a problem hiding this comment.
Fixed in bef1d72, red first in b65071d (with and without closesOnPan). The document and the host see a lift inside a closed root as a lift on the host, so the root judges a lift inside it on the way down and the host judges, on the way back up, one the root did not: a lift on the host itself. The host comes from a new shadowHostOf in utils/flatTree.
| }; | ||
| const handleCancel = () => { | ||
| startedOutside = false; | ||
| const handleCancel = (event: PointerEvent) => { |
There was a problem hiding this comment.
Non-blocking: unlike handleDown and handleUp, this does not check follows(event). Without closesOnPan, a pointercancel from an unfollowed type (a pen while pointerTypes is ['touch']) clears a followed touch's press, so its tap outside no longer closes the layer.
| ): () => void { | ||
| const doc = owner.ownerDocument; | ||
| let startedOutside = false; | ||
| // The pointer a pan follows, and where it went down. |
There was a problem hiding this comment.
Non-blocking (INV29): a name can carry this, for example let panStart: {id: number; x: number; y: number} | null, and the comment can go. The onOutside JSDoc above ("Called once for each press that starts and ends outside") also needs the closesOnPan case: "With closesOnPan, also when that press pans or is cancelled."
There was a problem hiding this comment.
Done in bef1d72: panStart, and the onOutside JSDoc and the file header name the pan close.
There was a problem hiding this comment.
Requesting changes for two fixes in the new root option.
- In a closed shadow root that sits inside another closed shadow root, the document sees a press as a press on the outer host, so the document's skip test and the host listener, which use the root's own host, miss it and a press that starts outside the layer and lifts on it closes the layer; use the outermost shadow host for both and add a nested case, since ContextMenu judges nested closed roots today.
shadowHostOftestsinstanceof ShadowRoot, which is false for a shadow root from a same-origin iframe or a Picture-in-Picture window, so the root's lift listener is never added and the same press closes the layer there; check bynodeType, as this module'sisShadowRootdoes, and add an iframe case.
Non-blocking: the closesOnPan JSDoc says only the first pointer counts, but a second finger outside is followed when the first went down inside the layer, and the move subscription could start at the outside press instead of with the watch. The single closed-root lift fix, the pan close at 10 px on either axis, the pointer-type filter and the unchanged default watches check out.
[Automated review]
| // The host judges, on the way back up, a lift the root did not judge. | ||
| let liftJudgedInRoot: Event | null = null; | ||
| const handleUp = (event: PointerEvent) => { | ||
| if (host == null || !event.composedPath().includes(host)) { |
There was a problem hiding this comment.
When root is a closed root nested inside another closed root, the document sees this lift as a lift on the outer host, so includes(host) is false and the document judges a lift on the layer as outside. Walking host.getRootNode() up to the outermost shadow host, and using that host here and for the host listener, keeps a press that starts outside and lifts on the layer from closing it.
There was a problem hiding this comment.
|
|
||
| /** The host of a shadow root, open or closed, and null for any other node. */ | ||
| export function shadowHostOf(node: Node): Element | null { | ||
| return node instanceof ShadowRoot ? node.host : null; |
There was a problem hiding this comment.
instanceof ShadowRoot is false for a shadow root from a same-origin iframe or a Picture-in-Picture window, so watchOutsidePress gets a null host and never adds the root's pointerup listener. node.nodeType === 11 && 'host' in node, as outsidePress.ts's isShadowRoot does, works across realms.
There was a problem hiding this comment.
Fixed in a7ded9d: shadowHostOf checks the node type and a host property, so a same-origin iframe's root has a host. The new iframe test, red first, also checks that a lift outside still closes there.
| /** | ||
| * A press outside also closes the layer as it begins to pan, or when the | ||
| * browser takes it. An anchored menu closes so under a finger. Only the | ||
| * first pointer counts, and a second one waits for it. |
There was a problem hiding this comment.
Non-blocking: when the first pointer went down inside the layer, or before the watch started, a second finger outside is followed and closes the layer as it pans, so "only the first pointer counts" is not quite true. Suggested: "A press outside also closes the layer as it begins to pan, or when the browser takes it. While one press outside is followed, other pointers are ignored."
There was a problem hiding this comment.
Taken in a7ded9d. The move subscription stays with the watch: it shares the window's one input listener, and returns at once while no press outside is followed.
There was a problem hiding this comment.
Requesting changes for two fixes in the new root option.
- In a closed root nested inside another, a lift on the outer root's content is judged only by the outermost host's bubble-phase listener, so a control there whose
pointeruphandler stops propagation leaves the layer open, and withclosesOnPanthe stale press then makes the watch ignore every later touch; judging those lifts in the capture phase (the same listener on each closed root betweenrootand the outermost host, each skipping a lift whose path holds the host below it) and a test that taps such a control would fix it. - The new
shadowHostOfinutils/flatTreerepeats the host check thatisShadowRootalready does inoutsidePress.ts, soisWithinLayershould walk withshadowHostOfand the privateisShadowRootshould go, leaving one realm-safe check.
Non-blocking: a few comments could follow the comment style more closely (the "An anchored menu closes so under a finger" sentence, the "on the way down / back up" idioms, and the long file header). The nested and other-window host lookups, the pan close at 10 px on either axis, the pointer-type filter and the unchanged default watches check out.
[Automated review]
| doc.addEventListener('pointerup', handleUp, true); | ||
| if (host != null) { | ||
| root?.addEventListener('pointerup', handleRootUp as EventListener, true); | ||
| host.addEventListener('pointerup', handleHostUp as EventListener); |
There was a problem hiding this comment.
In nested closed roots this bubble-phase listener is the only judge of a lift on the outer root's content, so a control there whose pointerup handler calls stopPropagation() keeps the lift from reaching it: the layer stays open, and with closesOnPan the stale panStart makes the watch ignore every later touch. Judging those lifts in the capture phase, with the same listener on each closed root between root and the outermost host (each skipping a lift whose path holds the host of the root below), and keeping this listener for a lift on the host itself, fixes it.
There was a problem hiding this comment.
Fixed in 9461404, red first in 94299af: the document and every shadow root out to it judge a lift in the capture phase, each skipping a lift that goes into the root below it, and a host judges only a lift on itself. The new test taps a control in the outer root that stops its pointerup, then checks that the next press still closes. A new primary pointer also starts a new press, so a lift that never arrives cannot leave the watch stuck.
| export function shadowHostOf(node: Node): Element | null { | ||
| return node.nodeType === node.DOCUMENT_FRAGMENT_NODE && 'host' in node | ||
| ? (node as ShadowRoot).host | ||
| : null; | ||
| } |
There was a problem hiding this comment.
isWithinLayer in Layer/outsidePress.ts still walks with its private isShadowRoot(current) ? current.host : null, the same check as this helper. Using current.parentNode ?? shadowHostOf(current) there and deleting isShadowRoot leaves one realm-safe check.
There was a problem hiding this comment.
Done in 9461404: isWithinLayer walks with shadowHostOf, and the private isShadowRoot is gone.
| readonly pointerTypes?: ReadonlyArray<string>; | ||
| /** | ||
| * A press outside also closes the layer as it begins to pan, or when the | ||
| * browser takes it. An anchored menu closes so under a finger. While one |
There was a problem hiding this comment.
Non-blocking (comment style): "An anchored menu closes so under a finger." is vague and names a caller; the sentences around it already say what the option does.
| // The document sees a lift inside closed roots as a lift on the outermost | ||
| // host, and so does that host. So the root judges a lift inside it on the way down. | ||
| // The host judges, on the way back up, a lift the root did not judge. |
There was a problem hiding this comment.
Non-blocking (comment style): "on the way down" and "on the way back up" are idioms. Suggested: "The document and the outermost host see a lift inside closed roots as a lift on that host. So the root judges a lift inside it in the capture phase. The host judges only a lift the root did not judge."
There was a problem hiding this comment.
Taken in 9461404, reworded for the capture-phase judges on every root.
There was a problem hiding this comment.
Requesting changes for one fix.
shadowRootsOutwardinutils/flatTreewalks the same shadow-root chain asopenShadowRootsOfinutils/selectionLock(whose host lookup for text under a shadow root also repeatsshadowHostOf), so the selection lock should call the two shared helpers, filtering to open roots, and drop its own copies; that change keeps today's behavior.
The capture-phase lift judgment on every closed root and the new-primary-pointer rule resolve the last round's findings. Non-blocking: the comment above shadowRootsOutward(root) in outsidePress.ts repeats the helper's JSDoc and can go.
[Automated review]
| * The shadow roots from `node` out to its document, innermost first, each with | ||
| * its host. Empty when `node` is not a shadow root. | ||
| */ | ||
| export function shadowRootsOutward( |
There was a problem hiding this comment.
openShadowRootsOf in utils/selectionLock.ts (lines 565-583) walks this same chain innermost first and keeps the open roots, and its lookup at lines 277-282 repeats shadowHostOf. Having the selection lock call these two helpers (filtering to open roots) and deleting its copies leaves one walk, with no behavior change:
function openShadowRootsOf(target: EventTarget | null): ShadowRoot[] {
if (target == null || typeof (target as Node).nodeType !== 'number') {
return [];
}
return shadowRootsOutward((target as Node).getRootNode())
.map(({root}) => root)
.filter(root => root.mode === 'open');
}There was a problem hiding this comment.
Done in abee181: the selection lock's open-root walk keeps the open roots of shadowRootsOutward, and its host lookup calls shadowHostOf. Its tests pass unchanged.
| {pointerTypes, closesOnPan = false, root = null}: OutsidePressOptions = {}, | ||
| ): () => void { | ||
| const doc = owner.ownerDocument; | ||
| // The shadow roots from `root` out to the document, innermost first. |
There was a problem hiding this comment.
Non-blocking (comment style): this restates shadowRootsOutward's name and its JSDoc; it can go.
There was a problem hiding this comment.
No blocking findings: the three opt-in options for the outside-press watch are internal and passed by no component yet, and the selection lock now calls the shared shadowHostOf and shadowRootsOutward helpers, which resolves the last round's finding.
The pan close at 10 px on either axis, the pointer-type filter and the closed-shadow-root judgment each have tests that fail before the change, and the existing callers' suites pass.
- Non-blocking: the
outsidePress.tsfile header grows to 20 lines and its prose still says a cancelled press is forgotten, which no longer holds withclosesOnPan; trimming it to its tags (see the inline note) fixes both.
[Automated review]
| * @input Listens for pointer presses on a document, and on the shadow roots | ||
| * around a given root; with closesOnPan, for a pan's moves through core's | ||
| * input stream |
There was a problem hiding this comment.
Non-blocking (INV29): this header is now 20 lines, past the 12-line budget, and the prose below it still says a press closes "only when it starts and ends outside" and that a pointercancel "forgets it", which is false with closesOnPan. watchOutsidePress's JSDoc already states the rule, so the header can keep only its tags:
/**
* @file outsidePress.ts
* @input Pointer presses on a document and on the shadow roots around a
* given root; with closesOnPan, pan moves from core's input stream
* @output watchOutsidePress; useOutsidePress, one watch per component; and
* the inside tests isPressInsideOwnedLayer and isFocusInsideOwnedLayer
* @position Internal to Layer: the outside-press dismissal for a manual
* popover whose owner decides what counts as inside. Not exported.
*/abee181 to
ed150e2
Compare
There was a problem hiding this comment.
Changes requested for one wording fix; the new options, their tests and the unchanged no-options path look right.
- Miss: the title and changeset say the watch can now "judge a press inside a shadow root", but a press inside an open shadow root was already judged correctly before this change and every new test uses a closed root, so say "closed shadow root" in both (for example "…and judge a press inside a closed shadow root, which the document sees as its host").
Non-blocking: the outsidePress.ts file header is still 20 lines, and its prose says a cancelled press is forgotten, which no longer holds with closesOnPan; see the earlier inline note on line 9.
[Automated review]
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: the layer's outside-press watch can follow chosen pointer types, close as a press that starts outside begins to pan (on either axis, at core's pan threshold) or when the browser takes it, and judge a press inside a shadow root there. No component passes these options yet, so every watch keeps today's behavior. Not exported from the package entry point. |
There was a problem hiding this comment.
Miss (claims match scope): a press inside an open shadow root was already judged as inside before this change, through the event's composed path, and every new test attaches a closed root. Suggested wording, here and in the title: "…or when the browser takes it, and judge a press inside a closed shadow root, which the document sees as its host."
There was a problem hiding this comment.
Done in 3e95544: "…and judge a press inside a closed shadow root, which the document sees as its host." The title says so too.
ad8e81b to
3e95544
Compare
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet.
The title and changeset now say "closed shadow root", which resolves the last review's only blocking finding, and the outsidePress.ts header keeps only its tags. The three options stay opt-in and no component passes them yet, so today's outside-press dismissal is unchanged.
Next step: CI skips drafts and unlabelled forks, so mark the pull request ready for review and have a maintainer add the ci:run label (it takes effect on the next push, or a close and reopen) so CI runs.
[Automated review]
3e95544 to
ae1d74a
Compare
* [Component Audit] Kbd: key badges grow with larger text * Kbd audit spec: measure contrast on the composited badge surface * Kbd audit: self-review follow-ups * Kbd: keep only the reason in the badge height comment
…meta#10269) test(cli): remove packed-CLI fixtures once per run, not in file hooks Two node suites removed thousands of files in a 30 s per-file hook, and under a full node run that removal can outlast the hook. The packed-old-core afterAll removed a 3,123-file copy of the CLI. The global-local afterAll removed the unpacked CLI and every pooled copy, about 12k entries. The packed-old-core fixture now holds the files `npm pack` puts in the CLI's tarball, not five whole folders. That drops about 460 test files npm never ships, and adds `host`, which it does ship. Three published asset folders that theme build never reads (templates, docs and icons, 1,896 files) are one link each to the package's own folder, or a copy where links are refused. So the fixture writes about 680 files. The node project's global setup now gives each run a folder. The unpacked CLI, its pooled copies and the packed-old-core fixture are made in it, and no per-file hook removes them. The global teardown moves the folder aside, which takes no time, and a detached child removes it, so close() never waits on a delete either. The next run's setup sweeps any trash that a stopped child left. When a test writes into a file that every placed copy shares, the fixtures now unpack only the changed paths again, not all 2,581 files. That repair runs inside the test, which a busy machine could make outlast its 30 s. The fixtures suite also unpacks the CLI in its own setup, as global-local does, not inside its first test. Tests cover the pack-list fixture, a teardown that never deletes through a link, the copy fallback, the run folder, the background removal and the sweep. The shared-file test covers the repair.
… and search focusable (meta#10192) * test(TransferListSelector): a disabled selector keeps its list controls focusable and blocks changes (red) * fix(TransferListSelector): a disabled selector keeps its list controls focusable The list fieldset no longer takes native disabled, which blurred a focused control when the selector became disabled while open. It carries aria-disabled, every row's transfer and reorder controls take their own focusable disabled state, and changes are ignored (spec:AST-085 FR1-FR4). * fix(TransferList): one isDisabled state disables every control focusably, including search TransferList takes isDisabled: every option is treated as fixed without its own constraint message, and the search field is disabled but focusable. TransferListSelector passes its disabled state down instead of rewriting options. * fix(TransferList): a list disabled mid-reorder ends the grab without any change finishReorder clears the session without committing while the list is disabled, so neither a cancel restore nor a pointer drop reaches onChange, and the grip shows no pressed state while disabled. * Merge main into lane4/transfer-selector-focusable-disabled
…ta#10264) test(core): six date and time cases pin each CLDR version's words CLDR 47 (ICU 77, as in Node 24.13) and CLDR 48 (ICU 78, as in Node 24.22) write some day-period, weekday and era names differently. Six cases hard-coded CLDR 48's text, so they failed on every run under ICU 77: - timeFormat: the Assamese afternoon, "PM" in CLDR 47 and "অপৰাহ্ন" in CLDR 48. - calendarDateParser: the Vietnamese short Tuesday, "Th 3" and "Thứ 3"; the English Amete Alem era, "ERA0" and "AA". - calendarDateParser.roundtrip: the Amharic narrow Minguo eras, "R.O.C." and "ROC", "Before R.O.C." and "BROC". - DateInputTouch.calendar: the English Ethiopic era in two pane labels, "ERA1" and "AM". Each case now expects the literal for the runtime's CLDR version (process.versions.cldr). Any other version expects CLDR 48's, so new locale data fails loudly. The cases that read text also check that the formatter writes the same literal.
…s close (meta#8186) * chore(core): the layer runtime's exit copy, for a touch presentation's close placeLayerExitCopy puts a static, inert copy of a closing layer where the layer was drawn, in the top layer, for a touch presentation's close to play on: the browser hides a popover the moment it closes. The copy keeps the layer's box, transform, presented look, overflow and scroll positions; has no ids, anchor names, form names or form owner; is inert and hidden from assistive technology; and its descendants' keyframe animations and transitions do not start again. A refused show during the closing toggle waits, hidden, until the toggle is over. A layer inside other layers gets its copy after the outermost one, with what it inherited there (its color scheme included), so a close that takes the layers around it too still draws the copy. That copy is shown again above the copy the same close then places for a layer around it. A layer has one copy. The layer-runtime record states it, and that the presentation plays its exit on the copy (AST-094 FR7). Core's shared inertCopy now takes every copied native control and form-associated custom element out of any form with form="", as its documentation says, so the swap's copies join no form either. It also sets anchor-name: none on each copied element that has an anchor name, so no live anchor gets a second element. * Merge branch 'main' into uikit/c26-layer-exit-copy * Merge branch 'main' into uikit/c26-layer-exit-copy * fix(core): the exit copy keeps no closed layer The exit copy found the inner layers to raise through a module-level set of every copied layer. A layer React removed on close stayed reachable there until a later close showed a copy. The inner layers are inside the closing layer, so they are now found there, and nothing keeps a layer once it is gone. Chromium 150 and later refuse a show during another popover's toggle, so the copy can open in the next frame. The browser spec's light-dismiss case now reads the copy after a frame, as its nested-close cases do. The cap check's comment now names only the copy cap. * docs(core): the exit copy's JSDoc states the empty-canvas rule once The empty-canvas rule stays in the first paragraph, and the inert-copy paragraph no longer repeats it or `inertCopy`'s own contract. * Merge main into the layer exit copy branch (layer-runtime.md verification table keeps both rows) * Merge main into the layer exit copy branch (meta#8147 landed) * Merge main into the layer exit copy branch (placeOver's beforeNode beside keepInherited) * Merge main into the layer exit copy branch (meta#10224 landed) * fix(core): the exit copy reads its host's style before the insertion keepInherited read the outer layer's parent's computed style right after the copy was inserted there: a style read after a write. It reads only that host and writes only on the still-detached copy, so it now runs before the insertion. What the copy inherits is unchanged. Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…ta#10289) docs(AST-059): anchored layers re-pick once after scrolling stops
…ed (meta#10274) perf(cli): list each folder once per read, and reuse it while unchanged Every folder a search, build or component read walks now lists through the listing cache: the docs, template, theme, icon and package walkers read it once per operation, where some read a folder twice or outside the cache. A later operation in the same process reuses a folder's listing while its stamp (size, times, inode) is the one it was kept with. A listing is kept only after its stamp has held for two seconds of this process's clock, so a change within one tick of a coarse file system clock still gets a new read. A reuse check records the folder before its stamp is taken, so a change between the two shows.
…root (meta#7931) * docs(spec): ref-first DOM styling target with explicit exemptions * Merge main into spec/ref-first-dom-style-props * docs(spec): AST-089 lists BottomSheetSwitcher as an exception, drops the bound TopNavMenu drawer-header ref, and words Icon's split BottomSheetSwitcher applies its dialog surface to every step's sheet dialog, so it joins the target-preservation exemptions. TopNavMenu and TopNavMegaMenu drawer headers bind their ref, so L10 keeps only SideNav. Icon styles its span and binds its ref to the svg. * docs(spec): AST-089 covers the Popover and PowerSearch sheet presentations, names the claims promotion amends, and collapses OQ1 E6 and E7 add the sheet paths of component:Popover DEC-8 and component:PowerSearch DEC-3. FR6 puts input-fields FR10 components outside the default. Current-state impact names public-component-api INV15 and AST-071 FR1. L7 says VisuallyHidden ignores xstyle; L8 says Icon's ref reaches the svg and its wrapper is unpromised. OQ1's choice moves into DEC-1. Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…ut (meta#10281) * chore(Carousel): the wheel listener's comment names a held row's reads While a flick holds the row, the listener's move reads the row's stops as well as its direction. The comment now says so. * chore(Carousel): the wheel listener's comment says when it reads layout While a flick holds the row, the listener's move reads the row's stops, its direction and the theme's motion tempo. The comment now states only that the listener reads no style or layout unless a flick holds the row.
…nested providers correctly (meta#10283) * [Component Audit] LayerProvider: its docs reach editors and describe nested providers correctly * LayerProvider audit: self-review follow-ups
…s back into it (meta#8187) * chore(core): the layer runtime's exit copy, for a touch presentation's close placeLayerExitCopy puts a static, inert copy of a closing layer where the layer was drawn, in the top layer, for a touch presentation's close to play on: the browser hides a popover the moment it closes. The copy keeps the layer's box, transform, presented look, overflow and scroll positions; has no ids, anchor names, form names or form owner; is inert and hidden from assistive technology; and its descendants' keyframe animations and transitions do not start again. A refused show during the closing toggle waits, hidden, until the toggle is over. A layer inside other layers gets its copy after the outermost one, with what it inherited there (its color scheme included), so a close that takes the layers around it too still draws the copy. That copy is shown again above the copy the same close then places for a layer around it. A layer has one copy. The layer-runtime record states it, and that the presentation plays its exit on the copy (AST-094 FR7). Core's shared inertCopy now takes every copied native control and form-associated custom element out of any form with form="", as its documentation says, so the swap's copies join no form either. It also sets anchor-name: none on each copied element that has an anchor name, so no live anchor gets a second element. * Merge branch 'main' into uikit/c26-layer-exit-copy * feat(Popover): under a finger, it grows out of its trigger and shrinks back into it Under a coarse primary pointer, Popover grows out of its trigger on the theme duration step nearest iOS's growth (`--duration-medium-min`) and the theme's entrance easing, and shrinks back into it on the theme's exit duration and easing, on the layer runtime's exit copy. A fine primary pointer keeps the layer's own entrance. * Merge branch 'main' into uikit/c26-layer-exit-copy * Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged) * feat(Popover): a touch during the growth is ignored While the popover grows, the layer runtime's input hold takes every touch, as UIKit ignores touches while it presents a popover. A tap outside does not dismiss it and reaches nothing under it, and a pinch still zooms the page. This folds in the hold that meta#8223 carried, so the growth and its hold land together. * fix(core): the exit copy keeps no closed layer The exit copy found the inner layers to raise through a module-level set of every copied layer. A layer React removed on close stayed reachable there until a later close showed a copy. The inner layers are inside the closing layer, so they are now found there, and nothing keeps a layer once it is gone. Chromium 150 and later refuse a show during another popover's toggle, so the copy can open in the next frame. The browser spec's light-dismiss case now reads the copy after a frame, as its nested-close cases do. The cap check's comment now names only the copy cap. * docs(core): the exit copy's JSDoc states the empty-canvas rule once The empty-canvas rule stays in the first paragraph, and the inert-copy paragraph no longer repeats it or `inertCopy`'s own contract. * Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged) * Merge main into the layer exit copy branch (layer-runtime.md verification table keeps both rows) * Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged) * Merge main into the layer exit copy branch (meta#8147 landed) * Merge the updated lower stack branch into the Popover branch (meta#8147 landed; uikit index header lists popover and zoomMedia) * Merge the updated lower stack branch into the Popover branch * Merge main into the layer exit copy branch (placeOver's beforeNode beside keepInherited) * Merge the updated lower stack branch into the Popover branch (meta#10224 landed) * Merge main into the layer exit copy branch (meta#10224 landed) * feat(Popover): the touch presentation starts in the next frame The growth and its input hold now start on the first frame after the open, not in the task that opens the popover, so the opening tap's task lays out no more than a mouse opening does. Folded from meta#8662, so the group's squashes apply in order: these lines are this pull request's own. * docs(Popover): record the next-frame window with the code The input hold starts on the first frame after the open, so a touch that lands within that frame reaches the page. spec:AST-094 and Popover's record state that window here, beside the code that opens it. Moved from meta#8662. * fix(core): the exit copy reads its host's style before the insertion keepInherited read the outer layer's parent's computed style right after the copy was inserted there: a style read after a write. It reads only that host and writes only on the still-detached copy, so it now runs before the insertion. What the copy inherits is unchanged. * Merge the updated lower stack branch into the Popover branch * perf(Popover): the growth reads its boxes and timing before it writes The frame that starts a touch presentation wrote the CSS entrance off and the start scale, read both boxes, wrote the origin and the start frame, then read the theme's entrance tokens. Each read after a write restyled or laid the page out again. It now reads both boxes and the entrance timing in one pass, then writes, the start frame first: the motion engine may read the element's will-change as it sets it. The box needs no reset first: the opening task already took the CSS entrance off, and a dismissal leaves the element at full size. Shown again during the dismissal, the popover reads its entrance timing before its first write too. Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…rink (meta#9823) * chore(core): the layer runtime's exit copy, for a touch presentation's close placeLayerExitCopy puts a static, inert copy of a closing layer where the layer was drawn, in the top layer, for a touch presentation's close to play on: the browser hides a popover the moment it closes. The copy keeps the layer's box, transform, presented look, overflow and scroll positions; has no ids, anchor names, form names or form owner; is inert and hidden from assistive technology; and its descendants' keyframe animations and transitions do not start again. A refused show during the closing toggle waits, hidden, until the toggle is over. A layer inside other layers gets its copy after the outermost one, with what it inherited there (its color scheme included), so a close that takes the layers around it too still draws the copy. That copy is shown again above the copy the same close then places for a layer around it. A layer has one copy. The layer-runtime record states it, and that the presentation plays its exit on the copy (AST-094 FR7). Core's shared inertCopy now takes every copied native control and form-associated custom element out of any form with form="", as its documentation says, so the swap's copies join no form either. It also sets anchor-name: none on each copied element that has an anchor name, so no live anchor gets a second element. * Merge branch 'main' into uikit/c26-layer-exit-copy * feat(Popover): under a finger, it grows out of its trigger and shrinks back into it Under a coarse primary pointer, Popover grows out of its trigger on the theme duration step nearest iOS's growth (`--duration-medium-min`) and the theme's entrance easing, and shrinks back into it on the theme's exit duration and easing, on the layer runtime's exit copy. A fine primary pointer keeps the layer's own entrance. * test(Popover): browser proofs of the touch presentation's grow and shrink Real-browser proofs, in Chromium and WebKit, that under a finger Popover grows out of its trigger on the theme's entrance tokens and shrinks back on its exit tokens, keeps advancing through a blocked main thread, does no Layout or Paint per frame, and cross-fades under Reduce Motion, with storyboards of both. * Merge branch 'main' into uikit/c26-layer-exit-copy * Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged) * Merge the updated lower stack branch into uikit/c27-popover-present-proofs (own diff unchanged) * Merge meta#8187's input hold into the Popover touch proofs * feat(Popover): a touch during the growth is ignored While the popover grows, the layer runtime's input hold takes every touch, as UIKit ignores touches while it presents a popover. A tap outside does not dismiss it and reaches nothing under it, and a pinch still zooms the page. This folds in the hold that meta#8223 carried, so the growth and its hold land together. * test(Popover): browser proofs of the hold while the popover grows A tap outside, a tap on the trigger and a pan while the popover grows do nothing, a pinch still zooms, and a storyboard shows the growth going on under a tap outside. Folded from meta#8223. * fix(core): the exit copy keeps no closed layer The exit copy found the inner layers to raise through a module-level set of every copied layer. A layer React removed on close stayed reachable there until a later close showed a copy. The inner layers are inside the closing layer, so they are now found there, and nothing keeps a layer once it is gone. Chromium 150 and later refuse a show during another popover's toggle, so the copy can open in the next frame. The browser spec's light-dismiss case now reads the copy after a frame, as its nested-close cases do. The cap check's comment now names only the copy cap. * docs(core): the exit copy's JSDoc states the empty-canvas rule once The empty-canvas rule stays in the first paragraph, and the inert-copy paragraph no longer repeats it or `inertCopy`'s own contract. * Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged) * Merge the updated lower stack branch into uikit/c27-popover-present-proofs (own diff unchanged) * test(Popover): the blocked-main-thread proof blocks from the page and asserts the verdict The page now starts the block once the growth has run a frame, and the proof asserts the harness verdict, which fails a judged part under 4 frames. Here the growth runs 600 ms at a constant speed, so each frame moves several pixels and a block that starts late on a loaded machine still lands inside it. * Merge main into the layer exit copy branch (layer-runtime.md verification table keeps both rows) * Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged) * Merge the updated lower stack branch into uikit/c27-popover-present-proofs (own diff unchanged) * Merge main into the layer exit copy branch (meta#8147 landed) * Merge the updated lower stack branch into the Popover branch (meta#8147 landed; uikit index header lists popover and zoomMedia) * Merge the updated lower stack branch into the Popover branch (meta#8147 landed; uikit index header lists popover and zoomMedia) * Merge the updated lower stack branch into the Popover branch * Merge main into the layer exit copy branch (placeOver's beforeNode beside keepInherited) * Merge the updated lower stack branch into the Popover branch * Merge the updated lower stack branch into the Popover branch (meta#10224 landed) * Merge main into the layer exit copy branch (meta#10224 landed) * Merge meta#8187 (next-frame start folded in) into the Popover touch proofs * feat(Popover): the touch presentation starts in the next frame The growth and its input hold now start on the first frame after the open, not in the task that opens the popover, so the opening tap's task lays out no more than a mouse opening does. Folded from meta#8662, so the group's squashes apply in order: these lines are this pull request's own. * test(Popover): browser proofs of the next-frame start The touch-motion spec's cases for the growth that starts on the first frame after the open, folded from meta#8662 with the change they prove. * docs(Popover): record the next-frame window with the code The input hold starts on the first frame after the open, so a touch that lands within that frame reaches the page. spec:AST-094 and Popover's record state that window here, beside the code that opens it. Moved from meta#8662. * Merge meta#8187 (next-frame records moved in) into the Popover touch proofs * fix(core): the exit copy reads its host's style before the insertion keepInherited read the outer layer's parent's computed style right after the copy was inserted there: a style read after a write. It reads only that host and writes only on the still-detached copy, so it now runs before the insertion. What the copy inherits is unchanged. * Merge the updated lower stack branch into the Popover branch * perf(Popover): the growth reads its boxes and timing before it writes The frame that starts a touch presentation wrote the CSS entrance off and the start scale, read both boxes, wrote the origin and the start frame, then read the theme's entrance tokens. Each read after a write restyled or laid the page out again. It now reads both boxes and the entrance timing in one pass, then writes, the start frame first: the motion engine may read the element's will-change as it sets it. The box needs no reset first: the opening task already took the CSS entrance off, and a dismissal leaves the element at full size. Shown again during the dismissal, the popover reads its entrance timing before its first write too. * Merge the updated lower stack branch into the Popover touch proofs Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…utes each (meta#10258) * ci: the Chromium interaction proofs run in as few legs as fit ten minutes each, from re-recorded times * ci: the leg count costs an unrecorded spec by its tests at the lane's mean time a test, and the recorded Chromium times add TabBar and three newer specs * ci: the Chromium legs' matrix holds six, since main's merge ref already plans four * ci: the plans cost files as their counts do, an unreadable spec stops the leg, and a test pins the Chromium count's arguments * ci: the plan reads the test counts the leg count wrote, since the head checkout has replaced the scope checkout by then * ci: a changed spec the merge commit no longer holds costs nothing in the leg count, and tests cover the tests.json hand-off
…eta#10273) test(cli): search goldens and the readme probe read the catalog once experimental-isolation's search goldens run in this process through runCli, as its docs topics do, so the seven queries share one read of the docs catalog instead of fourteen new processes. One spawned search checks that the in-process run prints what the bin prints. The icon-set project's ten searches run in one new process, through a new test helper (startCliSequence) that streams each result as it ends. Nothing read for the project without icon sets can answer for it, and its searches share one read of the catalog instead of ten new processes. One spawned search checks that process against the bin. The readme test probes one Core name of each kind of match outside an operation, instead of every eighth name and two variants of each.
…rver in another zone no longer causes a mismatch (meta#10226) * fix(Calendar, DateRangeInput): today is read after hydration, so a server in another zone no longer causes a mismatch Calendar and DateRangeInput read the local date during render. A server and a browser on different dates then render different months, today markers, or range text, and React hydration fails. A new internal useToday hook reads today through useSyncExternalStore: null on the server and in the render that hydrates server markup, the local date in every later render. Calendar marks no today and, with no focusDate or value, keeps its month grid's space hidden until the month is known. DateRangeInput shows the year until then. * chore(changeset): tag and credit the Calendar today changeset * fix(Calendar): keep the opening month once today is known, and prove hydration in a real browser After hydration a Calendar with no focusDate or value re-derived its opening month on every render, so a pick in the second month of a two-month view moved both months. The first render that knows today now stores the opening month. useToday builds on useIsHydrating instead of a second hydration gate. The layout claim is limited to the default six-row grid. New tests cover the empty month label, the unavailable month buttons, the skipped announcement and the two-month pick, and a real-browser fixture renders on one date and hydrates on the next. * test(Calendar): file the hydration fixture stories under Core Storybook files every story under a package root, so the fixture moves from Fixtures/ to Core/CalendarHydration.
…ent docs (meta#10227) test(visual-gate): regenerate the probe theme from the current component docs The probe theme covered 874 of the 892 documented theming selectors, so the visual gate did not probe the other 18. This reruns the existing generator (pnpm visual:probe-theme); its --check now passes (275 targets, 892 selectors). Fixture-only.
…OM (meta#10275) * test(core): a presentation resolves with no matchMedia, as in a test DOM A ContextMenu or a DropdownMenu with `presentation="adaptive"`, and the presentation hooks themselves, throw where the window has no matchMedia: `window.matchMedia is not a function`. An app's own jsdom tests have no matchMedia unless they stub it. These cases fail until the next commit. * fix(core): a presentation resolves with no matchMedia, as in a test DOM `usePresentation` and `useAdaptivePresentation` read the pointer and the compact size through matchMedia. Where the window has none, they now answer no match, so the component renders its default popover, as `useMediaQuery` already does. The check runs in the subscription and the snapshot, never during a server render. * test(core): an adaptive Popover renders with no matchMedia Popover resolves `presentation="adaptive"` through its own resolver, which still throws `window.matchMedia is not a function` in a DOM without matchMedia. coarsePointer's `matchesMediaQuery` reads any query and matches nothing there. The Popover case fails until the next commit. * fix(core): each presentation resolver reads matchMedia through one guard The two presentation hooks and Popover's resolver subscribe through coarsePointer's `watchMediaQuery` and read through `matchesMediaQuery`, which answer nothing where the window has no matchMedia. The check now lives in one owner, beside `isCoarsePointer`, so a later fix reaches all three.
* ci: the WebKit interaction scope covers utils/inputAge The gesture fixtures will bundle the gesture host's input-age helper, so a change to it runs the WebKit interaction proofs, as the other modules they bundle do. * Merge branch 'main' into ci/webkit-scope-input-age
There was a problem hiding this comment.
Approved pending CI: the unit, typecheck, lint, interaction, accessibility and visual checks pass on this head, and the final test check is still running.
The three opt-in options for the layer's outside-press watch (closesOnPan, pointerTypes and root) follow the touch-presentation spec's rule that a finger or pen outside a menu closes it as it begins to pan, stay internal, and are passed by no component yet, so every existing watch keeps today's behavior; the new cases fail against the watch on main and pass here.
- Non-blocking, for the caller in #8125:
closesOnPanfollows every pointer type unlesspointerTypesnarrows it, so pass both to keep a mouse's close at its release. - Non-blocking: the
flatTree.tsheader's "a shadow root's host" repeatsshadowHostOf's name; the earlier inline suggestion still applies. - #8125's branch does not contain this head yet; it still builds on this pull request's previous revision.
[Automated review]
|
/approve-spec 57f4e92 |
Stack: #9814 → #8125 → #9816 → #8133
Merge group: #9814, #8125, #9816, #8133
Maintainer impact
ContextMenu's touch model keeps the page free to scroll while its menu is up, so a touch outside the menu must close it as that touch begins to pan, as UIKit does (
spec:AST-094FR32). Today the layer's outside-press watch hears only presses and lifts, and it cannot follow one pointer type or judge a press inside a closed shadow root. #8125 needs all three. This pull request adds them alone, for size.Intended invariant
Every existing watch keeps today's behavior: a caller that passes no options gets the same press, lift and cancel handling. No component passes the new options here.
Change
watchOutsidePressanduseOutsidePresstake an optionalOutsidePressOptions:pointerTypes: the pointer types the watch follows; every type when omitted.closesOnPan: a press that starts outside also closes the layer as it begins to pan, once its larger axis reaches core's pan threshold (UIKIT_PAN.hysteresis, the pan recognizer's own test), or when the browser takes the press. While one press outside is followed, other pointers wait; a new primary pointer starts a new press. Its moves come through core's input stream (subscribeInput), not a new window listener.root: the shadow root the layer renders in, from any window. The document sees a press inside a closed root as a press on its host. So each root from it out to the document judges presses in the capture phase too: an inner root's judgment of a press's start stands, and each skips a lift that goes into the root below it. A host judges a lift on itself. Core's flat-tree owner gainsshadowHostOfandshadowRootsOutwardfor it, and the selection lock now walks shadow roots through them.Evidence
outsidePress.test.tscases fail at the test commit (6 failures), and the dismissal-count case fails before its fix.Scope
The changeset is
[chore] Internal:; the watch is not exported from the package entry point.Testing
Rebased onto main 9e559e9, which it conflicted with: #9988 added the per-document outside-dismissal count to
outsidePress.tsand its test, so the test file imports both and keeps both describe blocks, and the header lists both. The pan and the cancel close now count as a dismissal too, as #9988's lift does (two commits; red first, 1 of 22 cases fails before the fix).git range-diffagainst ae1d74a shows no other change. Local checks at e11abfe, on main 9e559e9: core typecheck and the Layer, ContextMenu, DropdownMenu, utils and motion units (2,779 passed); the full ui (21,278 passed) and node (16,672 passed) suites,CI=true pnpm lint(0 errors), check:changesets, check:knowledge, the Chromium interaction specs for ContextMenu, Layer, DropdownMenu, Dialog and Drawer (60 passed, 1 skipped), WebKit's ContextMenuLongPress, inputHold, preserveAnimations and DropdownMenuDragHighlight (35 passed, 8 skipped), and the ContextMenu, Layer and DropdownMenu a11y contracts (249 passed) ran at the stack's top, 6b4f1dc. Size (INV6), gzip: ContextMenu's import 85,035 → 85,016 B, DropdownMenu's unchanged (79,767 B), CSS unchanged. Red first: pointer-type filtering, the pan close (on each axis and at the threshold itself), the browser's cancel, the second finger, a new primary pointer after an unseen lift, the shadow-root judgment of a press and of a lift on the layer (in a closed root, in one inside another, and in a same-origin iframe's), a lift on an outer root's control that stops it, an unfollowed pointer's cancel,shadowHostOffor another window's root, and the pan or cancel close's dismissal count each fail before the change.