feat(settings): make typo Corrections a progressive-disclosure hierarchy#702
Conversation
The Writing pane showed "Hide Suggestions on Typo", "Offer Corrections on Typo", and "Automatically Fix Typos" as three flat peers, with the latter two merely dimmed when the first was off. The gate relationship was invisible: turning off "Hide Suggestions on Typo" silently killed the other two and the dictionary picker, with nothing to explain why. Reframe the section as the master/dependent idiom already used by EmojiPaneView. The gate now lives alone in a "Typos" section; the "Corrections" actions appear only once it is on; the "Spelling Dictionaries" picker appears only once a correction action that can consume them is on. Revealing each level when it becomes relevant (instead of dimming dependents in place) makes the dependency unmistakable. SpellingDictionaryPicker loses its now-redundant inline title and self-disabling logic since the enclosing Section supplies the header and the parent gates visibility.
80ed2af to
7790127
Compare
| if suggestionSettings.suppressCompletionsOnTypo { | ||
| Section("Corrections") { | ||
| Toggle(isOn: offerTypoCorrectionsBinding) { | ||
| SettingsRowLabel( | ||
| title: "Offer Corrections on Typo", | ||
| description: "Shows a green replacement you can apply with your accept key.", | ||
| systemImage: "checkmark.bubble" | ||
| ) | ||
| } | ||
| .settingsItem(.offerTypoCorrections) | ||
|
|
||
| Toggle(isOn: automaticallyFixTyposBinding) { | ||
| SettingsRowLabel( | ||
| title: "Automatically Fix Typos", | ||
| description: "After you press Space, replaces a misspelled word without requiring your accept key.", | ||
| systemImage: "checkmark.circle" | ||
| ) | ||
| Toggle(isOn: automaticallyFixTyposBinding) { | ||
| SettingsRowLabel( | ||
| title: "Automatically Fix Typos", | ||
| description: "After you press Space, replaces a misspelled word without requiring your accept key.", | ||
| systemImage: "checkmark.circle" | ||
| ) | ||
| } | ||
| .settingsItem(.automaticallyFixTypos) | ||
| } | ||
| .disabled(!suggestionSettings.suppressCompletionsOnTypo) | ||
| .settingsItem(.automaticallyFixTypos) | ||
|
|
||
| Divider() | ||
|
|
||
| SpellingDictionaryPicker(suggestionSettings: suggestionSettings) | ||
| .settingsItem(.spellingDictionaries) | ||
| // Dictionaries rank candidates for the two correction actions above, so they only | ||
| // appear once at least one action is on. With neither active, choosing a dictionary | ||
| // would have no observable effect. | ||
| if suggestionSettings.offerTypoCorrections || suggestionSettings.automaticallyFixTypos { | ||
| Section("Spelling Dictionaries") { | ||
| SpellingDictionaryPicker(suggestionSettings: suggestionSettings) | ||
| .settingsItem(.spellingDictionaries) | ||
| } | ||
| } |
There was a problem hiding this comment.
Search-navigation silently drops when dependent rows are hidden
When suppressCompletionsOnTypo is false, the .offerTypoCorrections, .automaticallyFixTypos, and .spellingDictionaries anchors are not in the view hierarchy. If the user finds one of those items via Settings search while the gate is off, SettingsNavigationModel.reveal(_:) correctly switches to the Writing pane, but SettingsPaneScaffold's scrollTo call targets a non-existent anchor and silently no-ops — no scroll and no arrival pulse. The user lands on the pane with nothing highlighted and no indication of why.
With the old flat layout, all three rows were always rendered (just .disabled()), so the scroll target was always present. This PR removes that guarantee. The same edge case exists today in EmojiPaneView for emoji sub-settings, so this is consistent with the existing pattern — but it's worth noting since the typo sub-settings are more likely to be searched independently (e.g., a user searching "Spelling Dictionaries" to choose a language before realising the gate exists).
A lightweight mitigation used elsewhere is to gate search result surfacing on the dependent condition, or to auto-enable the gate when search navigates directly to a dependent item.
Summary
The Writing pane rendered "Hide Suggestions on Typo", "Offer Corrections on Typo", and "Automatically Fix Typos" as three flat peers, with the latter two (and the dictionary picker) merely dimmed when the first was off. The gate relationship was invisible, so toggling off "Hide Suggestions on Typo" silently killed the other controls with nothing to explain why (it briefly read as a freeze).
This reframes the section as the master/dependent progressive-disclosure idiom already used by
EmojiPaneView: the gate lives alone in a Typos section; the Corrections actions appear only once it is on; the Spelling Dictionaries picker appears only once a correction action that can consume it is on. Revealing each level when it becomes relevant, instead of dimming dependents in place, makes the dependency unmistakable.Validation
swiftlint lint --quiet— exit 0xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build— BUILD SUCCEEDEDWritingPaneViewlayout offscreen viaNSHostingViewacross all three disclosure states and inspected each:Linked issues
None.
Risk / rollout notes
SuggestionSettingsModel, the persisted settings, or the typo/correction runtime behavior. The same three booleans drive the same gate logic; only their disclosure changes.SettingsIndex) keeps resolving these rows. Section headers are cosmetic and not referenced by search.SpellingDictionaryPickerdropped its inline "Spelling Dictionaries" title and its self-disabling logic; it is now always rendered live and gated purely by the parent's visibility condition. Its only caller isWritingPaneView.Greptile Summary
This PR converts the flat "Corrections" section in
WritingPaneViewinto a three-level progressive-disclosure hierarchy: a Typos gate section always shown, a Corrections section that appears only once the gate is on, and a Spelling Dictionaries section that appears only once at least one correction action is active.SpellingDictionaryPickerdrops its own inline heading anddisabled()guard in favour of pure parent-controlled visibility.SuggestionSettingsModel, all three persisted booleans, and the runtimeTypoGatelogic are untouched.EmojiPaneViewmaster/dependent idiom already established in the codebase, and all fourSettingsIndexentries remain registered so Settings search still resolves each row.Confidence Score: 4/5
The change is pure UI restructuring with no model, persistence, or runtime logic touched — low risk to merge.
All three typo-related settings — and the dictionary picker — are conditionally removed from the view hierarchy when their gate is off. This means a Settings search that resolves one of those items while the gate is off navigates to the Writing pane but cannot scroll to or pulse the target row, leaving the user stranded with no visual feedback. The rest of the change is clean and follows the established EmojiPaneView pattern.
The conditional rendering blocks in
WritingPaneView.swift(lines 94–123) are the area to examine for the search-navigation edge case.Important Files Changed
ifblocks and separateSectionheadings. No model or binding logic changed.dictionariesAffectCorrectionsguard and its.disabled()call, and dropped the inline "Spelling Dictionaries" heading; gating is now fully delegated to the parent's conditional render.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["WritingPaneView renders"] --> B["Section: Typos\n(always visible)\n─────────────────\nHide Suggestions on Typo toggle"] B --> C{suppressCompletionsOnTypo?} C -- "false (gate off)" --> D["No further typo sections rendered\n(Corrections + Dictionaries hidden)"] C -- "true (gate on)" --> E["Section: Corrections\n─────────────────\nOffer Corrections on Typo toggle\nAutomatically Fix Typos toggle"] E --> F{offerTypoCorrections\nor automaticallyFixTypos?} F -- "false (neither on)" --> G["Spelling Dictionaries section hidden"] F -- "true (at least one on)" --> H["Section: Spelling Dictionaries\n─────────────────\nSpellingDictionaryPicker\n(always live — no disabled logic)"]Reviews (1): Last reviewed commit: "feat(settings): make typo Corrections a ..." | Re-trigger Greptile