Skip to content

feat(native): Swift Button takes core's props; every view reads typed tokens - #8310

Merged
cixzhang merged 41 commits into
mainfrom
native/swift-button-props
Oct 30, 2026
Merged

cixzhang merged 41 commits into
mainfrom
native/swift-button-props

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What changes for people

An iOS developer gets core's whole Button:

  • width: .fill for a full-width button;
  • href, which makes the button a real link the system opens and VoiceOver reads as a link;
  • tooltip as a pointer tip and a VoiceOver hint;
  • elevation drawn with the theme's shadow;
  • isInterruptible, so a loading button can still be pressed to interrupt it.

With this, every Swift view that has a core contract takes its generated props, and every view reads the theme through typed tokens. The Login template's Sign in button fills its card through width: .fill, as on the web.

What this does

  • Button takes the generated ButtonProps (AST-093 FR5; Swift component API A2, A3, A4): a labeled initializer with the press last as a trailing closure (onClick), and the props initializer.
  • Each prop follows core:
    • href on an enabled button draws a SwiftUI Link styled with the Astryx button style; a disabled one stays a disabled button.
    • A loading link stays a link, focusable and busy, and opens nothing until loading ends unless it is interruptible. The open goes through SwiftUI's openURL, so onClick runs on every activation: tap, VoiceOver, or keyboard.
    • tooltip is SwiftUI's .help: a hover tip with a pointer and a hint VoiceOver reads; a tap belongs to the button, as with core's touchTrigger: auto.
    • elevation draws the theme's shadow; width is content or fill; size and variant come from typed tokens.
  • AstryxNodeView also draws Button and TextInput nodes.
  • G9:
    • Banner and the Login template read typed tokens.
    • The by-name lookups (astryx.color("…"), size, space, fontSize, font(size:…)) are no longer public.
  • New spec record, component:swift/Button (packages/native/apple/Sources/Astryx/Button/Button.spec.md, draft), per Cindy's decision that the loading rule belongs on the Swift Button spec. FR4 holds the loading-link rule: a loading Button with href stays a Link, keeps focus, shows its spinner, reads as busy, and does not open its destination until loading ends, unless the Button is interruptible, in which case each activation opens it. It never swaps to a button while busy (core Button FR9 and FR11). FR1–FR3 record the initializers, the loading presentation, and enabled or disabled href; FR5 says a script URL (javascript:, vbscript:) or an HTML document in a data: URL never opens, and the Button stays an enabled button. FR6: every disabled Button keeps its keyboard focus stop and shows its tooltip on focus, draws dimmed, reads as a dimmed button, and never runs onClick (core Button FR10, buttons family FR3); the focus stop is not measured until an app-hosted test can observe it. Each has a verification row. The record names the Apple guideline pages it follows (Buttons, Progress indicators, Accessibility) and the theme values Button reads. The shared component API record keeps no loading rule, only a pointer to Button FR4, and states no general rule. Theme environment's Current state describes the Astryx product.
  • README: the interim-exemption list is gone; Banner, which has no native contract yet, is the one view with hand-written parameters.

Landing order: after #8307 (Card) and #8308 (TextInput); #8305 (Button width) is on main. This branch carries #8308's commit so it builds; it is rebased onto main as each lands, leaving only its own change (8 files).

  • tooltip shows through the shared Astryx tooltip on hover and keyboard focus, its words the VoiceOver hint; no view uses .help.

Compatibility: breaking for canary callers. action:, fillsWidth:, icon:, and the hand-written AstryxButtonVariant and AstryxButtonSize give way to the generated ButtonProps (onClick:, width:, ButtonVariant, ButtonSize); the record states it.

Writing rubric results

  • R1 present tense: component:swift/Button and the component API pointer describe the system as it is.
  • R2 research in the record: none.
  • R3 evidence naming a change: none. Verification rows name tests, not pull requests.
  • R4 rejected alternatives: none.
  • R5 user impact first: the record's End-user impact row leads with a width that holds and a link that stays a link.
  • R6 open questions: none.

Captures (AST-093 FR5)

Capture set: iPhone 16 simulator, iOS 18.5, Xcode 16.4, built from this PR's head (6cad853). Each capture shows each variant and size, loading and disabled, full width, href as a link, loading and disabled links, each elevation, and two themes.

Light Dark Light, largest text Light, largest text, end Dark, largest text Dark, largest text, end
Button, Light Button, Dark Button, Light, largest text Button, Light, largest text, end Button, Dark, largest text Button, Dark, largest text, end

Accessibility tree: not measured in these captures; the Swift tests check names, traits and values.

Banner (statuses; two themes):

Testing

  • Swift tests:
    • both initializers build the same props;
    • an interruptible loading button takes presses, and a plain loading one does not;
    • href makes an enabled button a link and a disabled one a button;
    • a loading link stays a link and accepts no press, and an interruptible loading link opens;
    • every disabled Button keeps its focus stop and takes no press;
    • javascript:, vbscript:, and data:text/html destinations never open and leave an enabled button, while data:image and mailto: still open;
    • fill takes the whole width, in pixels;
    • elevation reads the theme's shadows;
    • Banner's typed colors;
    • the existing loading, disabled, 44-point, and Dynamic Type tests.
  • swiftc -typecheck of both modules, the tests, and the templates passes; the Swift tests run in CI on the iOS Simulator.

Every changed file is under packages/native/.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 26, 2026 11:21pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 27, 2026, 1:50 AM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: The accessibility audit did not run on this pull request. It runs after merges to main and before each release; pull requests that touch components run it with the ci:heavy-lanes label.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: moving Swift Button and TextInput onto their generated props follows the native guidelines, but two TextInput behaviors need fixing first.

  • A read-only type: .password field draws its value as plain, selectable text that VoiceOver reads; keep it masked, as core's read-only password input is.
  • The clear, status-tip and label-info buttons are hit only on their 12-point glyph; give each at least a 44 × 44 point hit area, as Button already does.

The Swift tests have not run on this head because CI only runs for pull requests into main, so approval also needs a green test-native-swift run once the branch targets main.

[Automated review]

Comment on lines +261 to +266
if isReadOnly {
// Read-only: the value at full strength, selectable, never editable.
SwiftUI.Text(props.value)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityValue(SwiftUI.Text(props.value))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A read-only type: .password field reaches this branch before type is read, so the password is drawn in clear text, can be selected and copied, and VoiceOver speaks it through accessibilityValue. Core keeps type="password" on its read-only input (AST-082 FR3: a prop is never honored only in part). Draw a masked value here for .password, without text selection or the raw accessibility value.

Comment on lines +201 to +208
if showsClear {
SwiftUI.Button {
props.onChange?("")
isFocused = true
} label: {
icon("close")
}
.buttonStyle(.plain)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This .plain button's hit area is the 12-point glyph from icon(_:); the 44-point row does not enlarge it. AST-082 FR11 asks for the platform's touch minimum: add .frame(minWidth: 44, minHeight: 44) and .contentShape(Rectangle()) here and in tipButton, as Button does.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: moving Swift Button and TextInput onto their generated props follows the native guidelines, but two TextInput behaviors need fixing first.

  • A read-only type: .password field draws its value as plain, selectable text that VoiceOver reads; keep it masked, as core's read-only password input is.
  • The clear, status-tip and label-info buttons are hit only on their 12-point glyph; give each at least a 44 × 44 point hit area, as Button already does.

Approval also needs a green test-native-swift run, which CI starts only once the branch targets main, and a TextInput case in the two-theme test.

[Automated review]

| A2 | A compile test builds every view from its generated `<Name>Props` with every prop set, plus each content slot's generated node type, and through the labeled initializer with a builder for each content slot, on iOS and on macOS. A source check finds no parameter that a view accepts and ignores; Switch's `size` exists on macOS only. |
| A3 | A test builds a view through each initializer with the same arguments and finds every `<Name>Props` field the same in both, comparing callbacks by invoking them. |
| A4 | `AnyView` does not appear in the components module. An enabled Button with `href` is a `Link` in the view tree and reads as a link to VoiceOver; a disabled Button with `href` reads as a dimmed button; an Avatar with `href` draws the same as without it. A disabled Item with `href` reads as a dimmed link and does not open. |
| A4 | `AnyView` does not appear in the components module. An enabled Button with `href` is a `Link` in the view tree and reads as a link to VoiceOver; a disabled Button with `href` reads as a dimmed button; an Avatar with `href` draws the same as without it. A disabled Item with `href` reads as a dimmed link and does not open. A loading Button with `href` stays a `Link`, keeps focus, reads as busy, and ignores a tap until loading ends. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This check drops A4's own exception: as written, an interruptible loading Button with href fails it, although A4 (and family:buttons FR5) let that button accept the press. Suggest "…reads as busy, and ignores a tap until loading ends unless the Button is interruptible."

Comment thread packages/native/apple/README.md Outdated
- `TextInput` takes a `Binding` where core has `value` and `onChange`.
the same across web and native. Every view with a core contract takes its
generated `<Name>Props` (AST-093 FR5) and has a labeled initializer that
builds the same props; `Banner` has no core contract yet, so its parameters

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Core does ship Banner (packages/core/src/Banner/Banner.tsx, Banner.spec.md); what it lacks is a native contract, since packages/native/src/components/ has no Banner.ts. Suggest "Banner has no native contract yet, so its parameters are hand-written."

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: moving Swift Button and TextInput onto their generated props follows the native guidelines, but two TextInput behaviors and one spec check still need fixing.

  • A read-only type: .password field draws its value as plain, selectable text that VoiceOver reads; keep it masked, as core's read-only password input is.
  • The clear, status-tip and label-info buttons are hit only on their 12-point glyph; give each at least a 44 × 44 point hit area, as Button already does.
  • The new A4 check row in component-api.md says a loading link ignores a tap until loading ends; add "unless the Button is interruptible", as A4's own text says.

Approval also needs a green test-native-swift run, which CI starts once the branch targets main, and a TextInput case in the two-theme test.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: moving Swift Button and TextInput onto their generated props follows the native guidelines, but two TextInput behaviors and one spec check row need fixing, and approval also needs a green Swift test run (CI's test-native-swift starts only for pull requests into main), captures, and TextInput and Banner two-theme checks.

  • A read-only type: .password field draws its value as plain, selectable text that VoiceOver reads; keep it masked, as core's read-only password input does.
  • The clear and tip buttons hit-test only their 12-point glyph; give them a 44-point frame and content shape, as Button has.
  • The new A4 check row in component-api.md should keep “unless the Button is interruptible”, as the A4 text above it does.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: moving Swift Button and TextInput onto their generated props follows the native guidelines, but two TextInput behaviors, one spec check row and one README sentence need fixing. The TextInput fixes belong in #8308 and reach this branch on rebase; approval also needs a green test-native-swift run, which CI starts only once the branch targets main, plus the TextInput and Banner two-theme checks and a Banner capture that the native guidelines ask for.

  • A read-only type: .password field draws its value as plain, selectable text that VoiceOver reads; keep it masked, as core's read-only password input does.
  • The clear and tip buttons take taps only on their 12-point glyph; give them a 44-point frame and content shape, as Button has.
  • The new A4 check row in component-api.md should keep "unless the Button is interruptible", as the A4 text above it does.
  • The README should say Banner has no generated native contract yet, since core's Banner has a current spec.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 54df851

…nvironment count Banner and Icon; Banner two-theme test
…on; the Swift Button record names its guideline pages and theme values
…and its focus; theme environment counts eight components
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 18f9ea9

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval recommended: the Swift Button now takes core's generated ButtonProps, matches core's Button contract and the draft component:swift/Button record an engineering owner approved at this head, and its tests pass on macOS and the iOS Simulator. This branch carries the TextInput commits of #8308, so it should land after #8308; rebasing onto main then creates a new head that needs a fresh /approve-spec with its full SHA from an engineering owner (the author may give it as one). A relative href such as /settings becomes a link that opens nothing, which no spec defines yet, so a follow-up spec line for native relative destinations would help.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 18f9ea96 Deployed Oct 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant