Skip to content

fix(search-bar): style the typed text and drive Cancel from focus - #31

Merged
romchornyi merged 4 commits into
masterfrom
fix/searchbar-text-style
Oct 31, 2026
Merged

romchornyi merged 4 commits into
masterfrom
fix/searchbar-text-style

Conversation

@romchornyi

@romchornyi romchornyi commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

Closes #26.

SearchBar styled only its placeholder. The text you type fell back to the system body font (17pt) in .primary, while the placeholder is .subhead (15pt). So the line changed size on the first keystroke and did not use the design-system colour. dashwallet-ios uses this field in LocalCurrencyView and SelectCoinView.

What was done?

  • Typed text gets .dashFont(.subhead), Color.dash.primaryText and the primaryText tint, then .focused: the same modifiers, in the same order, as AddressFieldView, so the two fields render typed text the same way. .dashFont adds about 1pt of line-height padding, which fits inside the 40pt field; the text stays vertically centred.
  • Cancel button:
    • It is driven by @FocusState isFocused directly. The @State isEditing copy is gone, along with the onAppear sync, the onChange and both withAnimation blocks.
    • One .animation(_:value: isFocused) now covers what was animated three times.
    • The removed onAppear sync never had any effect: @FocusState is always false on first appearance, and the host cannot focus this field from outside.
  • Previews: empty, empty in dark mode, typed, and typed in dark mode.
  • Docs: docs/buttons-and-inputs.md describes the focus-driven Cancel button, the typed-text style, and that focus is internal to the component.

How Has This Been Tested?

  • Conventions check, swift build (macOS), the iOS Release build for devices and the unit tests on an iOS 18.6 simulator are clean, with no warnings.
  • Rendered on an iOS 18.6 simulator, master and this branch side by side, in light and dark, in four states: empty, focused, typed, typed + focused. On master the typed text is visibly larger than the placeholder; on this branch they match. The field stays vertically centred, Cancel appears only while focused, and dark mode uses primaryText.
  • A focus-sequence review covered gaining focus by tap, hardware keyboard and VoiceOver, and losing it by Cancel, a tap outside, or the sheet or host going away. Each sequence ends with the same Cancel state as before.
  • dashwallet-ios develop (LocalCurrencyView, SelectCoinView) sets no font or colour on SearchBar, so nothing there is overridden.

Before / After

iOS 18.6 simulator, rendered by a test harness. "Focused" shows the Cancel button.

Before After
Typed, light
Typed, dark
Typed + focused, light
Typed + focused, dark
Empty + focused, light
Empty + focused, dark
  • No visual change (refactor, tests, docs). Say why in "What was done?".

Breaking Changes

None. The public API is unchanged.

One behaviour to know: typed text is now a fixed 15pt, like every Font.dash token, the placeholder and AddressFieldView. Before, it was the system body style, which scaled with Dynamic Type. The docs say so.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • The Cancel button now appears while the search field is focused. Tapping it clears the text and removes focus.
    • Search text now uses the Dash subhead font and primary text color; its font size does not scale with Dynamic Type.
  • Documentation
    • Updated the SearchBar guide to describe Cancel behavior, focus handling, and text styling.

The field styled only its prompt. Typed text fell back to the system
body font in `.primary`, so the line grew from 15pt to 17pt on the first
keystroke and did not use the design-system colour. It now uses
`.dashFont(.subhead)` and `Color.dash.primaryText`, the same modifiers as
`AddressFieldView`.

The Cancel button followed a `@State` copy of the focus state, synced in
`onAppear` and `onChange`, and each focus change was animated three times.
It now reads `isFocused` directly, with one `.animation(value:)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 402db65 and 298c31d.

Files selected for processing: 2
  • Sources/DashUIKit/Components/SearchBar.swift
  • docs/buttons-and-inputs.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

SearchBar now uses focus to control Cancel visibility, applies Dash subhead styling to typed text, and includes empty and typed previews in light and dark color schemes. The documentation describes the focus behavior and text styling.

Changes: SearchBar

Layer / File(s) Summary
Focus behavior and text presentation
Sources/DashUIKit/Components/SearchBar.swift, docs/buttons-and-inputs.md
Cancel visibility and animation now use isFocused instead of a separate isEditing state. The cancel action clears text and resets focus. Typed text uses the Dash subhead font and primary text color. Previews show empty and typed fields in light and dark color schemes. Documentation describes the internal focus state and notes that the font does not scale with Dynamic Type.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related PRs

Suggested reviewers: jeanpierreroma

Merge Risk: Minimal · up to 298c3

The search text now matches the documented fixed-size typography, and Cancel follows focus directly. No merge-blocking issue remains established.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the two main changes: typed-text styling and deriving Cancel visibility from focus.
Linked Issues check Passed Issue [#26] requires design-system styling for typed text and removal of the mirrored isEditing state. SearchBar.swift applies .dashFont(.subhead), Color.dash.primaryText, and the primary-text…
Out of Scope Changes check Passed The changes stay within issue [#26]. The added previews support the requested visual states. The documentation records the changed focus and text styling behavior. No unrelated source or public API ch…
Docstring Coverage Passed No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Full details: Linked Issues check

Explanation

Issue [#26] requires design-system styling for typed text and removal of the mirrored isEditing state. SearchBar.swift applies .dashFont(.subhead), Color.dash.primaryText, and the primary-text tint. It drives Cancel visibility and the single animation from isFocused. The Cancel action clears text and focus. The documentation records the focus behavior and fixed text style. No additional automated test requirement appears in the issue.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue [#26]. The added previews support the requested visual states. The documentation records the changed focus and text styling behavior. No unrelated source or public API changes appear in the reviewed diff.

Full details: Docstring Coverage

Explanation

No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 1 files. (1 skipped: 1 unsupported.)

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@romchornyi-pitcrew romchornyi-pitcrew 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.

✅ no blockers · no majors · 6 non-blocking recommendations — reviewed at e5a69b19d4

Level Verdict
1 · Idea ✅ valid #26 is real: typed text fell back to system body 17pt in .primary while the prompt is subhead 15pt; isEditing only mirrored @FocusState
2 · Architecture ✅ right layer The defect and the fix both live in the DashUIKit component; callers in dashwallet-ios set no font or colour on SearchBar
3 · Completeness ✅ complete Only SearchBar and AddressFieldView hold a TextField at this head, and AddressFieldView already uses this styling; both points of #26 are covered
4 · Code ✅ clean Cancel is driven by focus with one implicit animation on the same subtree as before; typing does not trigger it
6 non-blocking recommendations
  1. Placeholder colour has no dark variant —
    .foregroundStyle(Color.dash.black1000Alpha30)
    — Black1000Alpha30 defines only a universal colour (near-black, 30% alpha), so in dark mode the empty-field placeholder almost vanishes, while typed text is now the adaptive primaryText. The PR leaves the placeholder as it was, and
    .foregroundStyle(Color.dash.black1000Alpha30),
    uses the same token, but the new "Typed, dark" preview never shows the empty dark state. An issue for an adaptive placeholder token, shared by both fields, would track it. (verified against the colorset)
  2. Shared input-field style —
    TextField(
    text: $text,
    // A prompt must stay a `Text`; `dashFont` returns `some View`, and a
    // line height cannot be applied to `Text` anyway.
    prompt: Text(placeholder)
    .font(Font.dash.subhead)
    .foregroundStyle(Color.dash.black1000Alpha30)
    ) {
    EmptyView()
    }
    .focused($isFocused)
    .dashFont(.subhead)
    .foregroundStyle(Color.dash.primaryText)
    .tint(Color.dash.primaryText)
    — the prompt, .dashFont(.subhead), foregroundStyle and tint chain, along with its comment, is copied from
    "",
    text: $text,
    // A prompt must stay a `Text`; `dashFont` returns `some View`, and a
    // line height cannot be applied to `Text` anyway.
    prompt: Text(placeholder)
    .font(Font.dash.subhead)
    .foregroundStyle(Color.dash.black1000Alpha30),
    axis: .vertical
    )
    .lineLimit(1...2)
    .dashFont(.subhead)
    .textInputAutocapitalization(.never)
    .autocorrectionDisabled()
    .foregroundStyle(Color.dash.primaryText)
    .tint(Color.dash.primaryText)
    . A small shared modifier would keep the two fields from drifting apart, for example when item 1 is fixed.
  3. .dashFont on a single-line field — — on one line dashLineHeight adds only delta / 2 ≈ 1pt of vertical padding outside the text field, and lineSpacing does nothing. .font(Font.dash.subhead) alone would match the prompt exactly. AddressFieldView needs .dashFont because it can wrap to two lines. Harmless either way.
  4. Previews for the states this PR changes —
    #Preview("Empty") {
    @Previewable @State var text = ""
    SearchBar(text: $text)
    .padding()
    }
    #Preview("Typed") {
    @Previewable @State var text = "Dash"
    SearchBar(text: $text)
    .padding()
    }
    #Preview("Typed, dark") {
    @Previewable @State var text = "Dash"
    SearchBar(text: $text)
    .padding()
    .background(Color.dash.primaryBackground)
    .preferredColorScheme(.dark)
    }
    — no preview shows Cancel visible or the empty field in dark mode. The repo's CLAUDE.md asks for a preview of every state. The focused state cannot be forced through the private @FocusState, so a note in the docs that it is checked on a simulator would do.
  5. No test for the Cancel logic —
    if isFocused {
    cancelButton
    .transition(.move(edge: .trailing).combined(with: .opacity))
    }
    and
    action: {
    text = ""
    isFocused = false
    },
    — the tests box is unchecked and Tests/ has nothing for SearchBar. Nothing would fail if a later change brought back a lagging state copy, or cleared the text without dropping focus.
  6. Clear-button removal on Cancel —
    text = ""
    isFocused = false
    — text = "" and isFocused = false are separate writes. If SwiftUI applies the focus write in a later update, the clear button disappears without animation while Cancel animates out. Worth a look on a simulator. (not verified, runtime behaviour)

The fixed 15pt size for typed text (no Dynamic Type) is a deliberate trade-off. The PR description and the docs both state it, and it matches every Font.dash token, so it is not listed as a finding.

Review details

Tier: normal (UI component logic + styling). Levels 1–3 inline. Level 4: general lane (/code-review high) and the UI specialist lane (states, animation, Dynamic Type, VoiceOver, dark mode, tokens, docs), with the class boundary set to every text-input component in DashUIKit. The verifier checked the dark-mode placeholder claim against Black1000Alpha30.colorset and the padding claim against dashLineHeight. The Cancel-animation point stays unverified. CI was green at this head.

🤖 Reviewed with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@romchornyi romchornyi mentioned this pull request Oct 31, 2026
5 tasks done
romchornyi and others added 2 commits October 31, 2026 17:36
… an empty dark preview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@romchornyi-pitcrew

romchornyi-pitcrew commented Oct 31, 2026 •

Copy link
Copy Markdown

✅ Review posted for commit 298c31d · triage: trivial · approve · took 0 min

@romchornyi-pitcrew romchornyi-pitcrew 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.

✅ no blockers · no majors · 3 non-blocking recommendations — reviewed at 298c31dd3e

Level Verdict
1 · Idea ✅ valid Unchanged since the last review
2 · Architecture ✅ right layer Unchanged
3 · Completeness ✅ complete Unchanged
4 · Code ✅ clean The PR's own new commits (421e134, 298c31d) move .focused after the style modifiers, which changes no behaviour, and add a preview and one doc line. The rest of the range is the merge of master. CI is green at this head

Closed since the last review (bot review on e5a69b1)

  • Previews for the states this PR changes: an "Empty, dark" preview is added, which shows the placeholder on the dark background. The focused state cannot be forced through the private @FocusState, and the docs now say so ("Focus is internal").
    #Preview("Empty, dark") {
    @Previewable @State var text = ""
    SearchBar(text: $text)
    .padding()
    .background(Color.dash.primaryBackground)
    .preferredColorScheme(.dark)
    }
  • The field's modifiers are ordered like AddressFieldView's, so the two read the same.
    }
    .dashFont(.subhead)
    .foregroundStyle(Color.dash.primaryText)
    .tint(Color.dash.primaryText)
    .focused($isFocused)
3 non-blocking recommendations (open from the last review)
  1. The placeholder colour has no dark variant —
    .foregroundStyle(Color.dash.black1000Alpha30)
    — Black1000Alpha30 is still universal-only. The new "Empty, dark" preview now shows the near-invisible placeholder that the last review described. AddressFieldView uses the same token. No issue tracks it yet. One issue for an adaptive placeholder token, shared by both fields, would keep it from getting lost.
  2. A shared input-field style — the two fields now match in order, but they are still two copies. A small shared modifier would keep them from drifting, for example when item 1 is fixed.
  3. No test for the Cancel logic — still no test in Tests/ for showing and hiding Cancel or for the Cancel action. Now that #40 adds snapshot tests, a SearchBar empty / typed reference in light and dark would pin the visual half at least.
Review details

Re-review of the bot review on e5a69b1. The range has ten commits, but eight of them are master (#30, #33–#37, #39) merged in. The bot reviewed each of those on its own PR. The PR's own diff against master is still SearchBar.swift and docs/buttons-and-inputs.md. Following the re-review rule, only level 4 was re-checked, inline, against 421e134 and 298c31d. No lane was re-run for a modifier reorder, a preview and a doc line. Dropped from the last list: the .dashFont padding nit, and the clear-button animation note, which stays unverified and needs no change.

🤖 Reviewed with Claude Code

@romchornyi
romchornyi merged commit 7dcacb0 into master Oct 31, 2026
11 checks passed
@romchornyi
romchornyi deleted the fix/searchbar-text-style branch October 31, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SearchBar: style the typed text and drop the isEditing mirror of the focus state

2 participants