refactor: move rule editor actions into the navigation bar
- Both editor modes commit via a checkmark confirmation button (role: .confirm) in the navigation bar — accessibility labels 'Add Rule' (create) and 'Done' (edit); replaces the bottom 'Add Rule' button, so nothing floats over the form anymore - Edit mode gains an ellipsis 'Rule Actions' menu next to the checkmark holding Disable/Enable Rule and destructive Delete Rule; the red form rows are removed - Tests pin the checkmark to the navigation bar with its label and drive disable/delete through the menu; the swipe-to-reach workarounds are gone. 95 passing - Spec §6 updated Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,9 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// The rule editor as a plain Form, always pushed inside a NavigationStack
|
/// The rule editor as a plain Form, always pushed inside a NavigationStack
|
||||||
/// (New Rule flow and detail editing both push it). Creation commits with a
|
/// (New Rule flow and detail editing both push it). Both modes commit via a
|
||||||
/// prominent "Add Rule" button; editing uses a toolbar Done plus red
|
/// checkmark confirmation button in the navigation bar; edit mode adds an
|
||||||
/// Disable/Delete rows.
|
/// ellipsis "Rule Actions" menu (Disable/Enable, Delete) next to it.
|
||||||
struct RuleEditorView: View {
|
struct RuleEditorView: View {
|
||||||
enum Mode: Equatable {
|
enum Mode: Equatable {
|
||||||
case create
|
case create
|
||||||
@@ -27,20 +27,6 @@ struct RuleEditorView: View {
|
|||||||
Form {
|
Form {
|
||||||
nameSection
|
nameSection
|
||||||
sections
|
sections
|
||||||
if case .edit(let isEnabled) = mode {
|
|
||||||
Section {
|
|
||||||
Button(isEnabled ? "Disable Rule" : "Enable Rule") {
|
|
||||||
onToggleEnabled?()
|
|
||||||
}
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.accessibilityIdentifier("toggleEnabledButton")
|
|
||||||
|
|
||||||
Button("Delete Rule", role: .destructive) {
|
|
||||||
onDelete?()
|
|
||||||
}
|
|
||||||
.accessibilityIdentifier("deleteRuleButton")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -50,32 +36,41 @@ struct RuleEditorView: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.accessibilityIdentifier("ruleEditorTitle")
|
.accessibilityIdentifier("ruleEditorTitle")
|
||||||
}
|
}
|
||||||
if case .edit = mode {
|
if case .edit(let isEnabled) = mode {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Menu {
|
||||||
|
Button(isEnabled ? "Disable Rule" : "Enable Rule") {
|
||||||
|
onToggleEnabled?()
|
||||||
|
}
|
||||||
|
Button("Delete Rule", role: .destructive) {
|
||||||
|
onDelete?()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis")
|
||||||
|
}
|
||||||
|
.accessibilityLabel("Rule Actions")
|
||||||
|
.accessibilityIdentifier("ruleActionsMenu")
|
||||||
|
}
|
||||||
|
}
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
Button("Done") {
|
switch mode {
|
||||||
onCommit(draft.sanitized())
|
case .create:
|
||||||
}
|
Button(role: .confirm) {
|
||||||
.accessibilityIdentifier("doneButton")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.safeAreaInset(edge: .bottom) {
|
|
||||||
if mode == .create {
|
|
||||||
Button {
|
|
||||||
onCommit(draft.sanitized())
|
onCommit(draft.sanitized())
|
||||||
} label: {
|
} label: {
|
||||||
Text("Add Rule")
|
Image(systemName: "checkmark")
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.accessibilityLabel("Add Rule")
|
||||||
.controlSize(.large)
|
|
||||||
.accessibilityIdentifier("commitRuleButton")
|
.accessibilityIdentifier("commitRuleButton")
|
||||||
.padding(.horizontal)
|
case .edit:
|
||||||
.padding(.vertical, 10)
|
Button(role: .confirm) {
|
||||||
.frame(maxWidth: .infinity)
|
onCommit(draft.sanitized())
|
||||||
// Bar material so scrolling form content doesn't collide
|
} label: {
|
||||||
// with the floating button.
|
Image(systemName: "checkmark")
|
||||||
.background(.bar)
|
}
|
||||||
|
.accessibilityLabel("Done")
|
||||||
|
.accessibilityIdentifier("doneButton")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingAppPicker) {
|
.sheet(isPresented: $showingAppPicker) {
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ final class RuleCreationUITests: XCTestCase {
|
|||||||
XCTAssertEqual(app.staticTexts["ruleEditorTitle"].waitToAppear().label, "In the Zone")
|
XCTAssertEqual(app.staticTexts["ruleEditorTitle"].waitToAppear().label, "In the Zone")
|
||||||
XCTAssertTrue(app.staticTexts["During this time"].exists)
|
XCTAssertTrue(app.staticTexts["During this time"].exists)
|
||||||
|
|
||||||
// Hold to commit saves the rule and returns home.
|
// The confirmation checkmark lives in the navigation bar and carries
|
||||||
app.buttons["commitRuleButton"].waitToAppear().tap()
|
// a descriptive accessibility label.
|
||||||
|
let commit = app.navigationBars.buttons["commitRuleButton"].waitToAppear()
|
||||||
|
XCTAssertEqual(commit.label, "Add Rule")
|
||||||
|
commit.tap()
|
||||||
app.buttons["ruleCard-In the Zone"].waitToAppear()
|
app.buttons["ruleCard-In the Zone"].waitToAppear()
|
||||||
XCTAssertFalse(app.element("emptyRulesCard").exists)
|
XCTAssertFalse(app.element("emptyRulesCard").exists)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ final class RuleManagementUITests: XCTestCase {
|
|||||||
|
|
||||||
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
|
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
|
||||||
app.buttons["editRuleButton"].waitToAppear().tap()
|
app.buttons["editRuleButton"].waitToAppear().tap()
|
||||||
// The disable/delete rows sit at the bottom of the form.
|
|
||||||
app.staticTexts["ruleEditorTitle"].waitToAppear()
|
// Disable lives in the ellipsis menu in the navigation bar.
|
||||||
app.swipeUp()
|
let actionsMenu = app.navigationBars.buttons["ruleActionsMenu"].waitToAppear()
|
||||||
app.buttons["toggleEnabledButton"].waitToAppear().tap()
|
XCTAssertEqual(actionsMenu.label, "Rule Actions")
|
||||||
|
actionsMenu.tap()
|
||||||
|
app.buttons["Disable Rule"].waitToAppear().tap()
|
||||||
|
|
||||||
// The detail caption now reports the rule as disabled.
|
// The detail caption now reports the rule as disabled.
|
||||||
let status = app.staticTexts["detailStatusLabel"].waitToAppear()
|
let status = app.staticTexts["detailStatusLabel"].waitToAppear()
|
||||||
@@ -66,10 +68,10 @@ final class RuleManagementUITests: XCTestCase {
|
|||||||
|
|
||||||
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
|
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
|
||||||
app.buttons["editRuleButton"].waitToAppear().tap()
|
app.buttons["editRuleButton"].waitToAppear().tap()
|
||||||
// The disable/delete rows sit at the bottom of the form.
|
|
||||||
app.staticTexts["ruleEditorTitle"].waitToAppear()
|
// Delete lives in the ellipsis menu in the navigation bar.
|
||||||
app.swipeUp()
|
app.navigationBars.buttons["ruleActionsMenu"].waitToAppear().tap()
|
||||||
app.buttons["deleteRuleButton"].waitToAppear().tap()
|
app.buttons["Delete Rule"].waitToAppear().tap()
|
||||||
|
|
||||||
app.buttons["newRuleButton"].waitToAppear()
|
app.buttons["newRuleButton"].waitToAppear()
|
||||||
XCTAssertFalse(
|
XCTAssertFalse(
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ reference for *what* the feature does; presentation now maps as follows:
|
|||||||
| Apps home | `NavigationStack` + `List`; "Blocked Apps" and "Rules" sections; **rules are list rows** (kind icon, name, block summary, trailing live status — green when active); "+" toolbar button |
|
| Apps home | `NavigationStack` + `List`; "Blocked Apps" and "Rules" sections; **rules are list rows** (kind icon, name, block summary, trailing live status — green when active); "+" toolbar button |
|
||||||
| Rule detail | Sheet with inline nav title (name + "Schedule, 6h left" caption), `LabeledContent` rows, "Edit Rule" row pushes the editor; hard-locked rules show a lock row instead |
|
| Rule detail | Sheet with inline nav title (name + "Schedule, 6h left" caption), `LabeledContent` rows, "Edit Rule" row pushes the editor; hard-locked rules show a lock row instead |
|
||||||
| New Rule | `List` with a "Rule Type" section and preset sections as plain rows; editor pushed via `navigationDestination(item:)` |
|
| New Rule | `List` with a "Rule Type" section and preset sections as plain rows; editor pushed via `navigationDestination(item:)` |
|
||||||
| Rule editor | Native `Form`: an inline **Name text field** at the top (no separate rename button; empty names fall back to the kind default), `DatePicker` rows, full-width day-circle row (≥44pt tap targets) with the summary in the section header, toggle rows with footers, stepper rows. Create commits with a prominent **"Add Rule"** button on a bar-material inset (replaces Hold to Commit); edit uses toolbar **Done** plus red Disable/Delete rows |
|
| Rule editor | Native `Form`: an inline **Name text field** at the top (no separate rename button; empty names fall back to the kind default), `DatePicker` rows, full-width day-circle row (≥44pt tap targets) with the summary in the section header, toggle rows with footers, stepper rows. Both modes commit via a **checkmark** in the navigation bar (labels: "Add Rule" / "Done"; replaces Hold to Commit). In edit mode an **ellipsis menu** ("Rule Actions") next to the checkmark holds Disable Rule and the destructive Delete Rule |
|
||||||
| Onboarding / app picker | System styling, `.borderedProminent` buttons, default color scheme (no forced dark, default accent) |
|
| Onboarding / app picker | System styling, `.borderedProminent` buttons, default color scheme (no forced dark, default accent) |
|
||||||
|
|
||||||
Dropped custom components: `Theme`, `HoldToCommitButton`, `RuleCardView`,
|
Dropped custom components: `Theme`, `HoldToCommitButton`, `RuleCardView`,
|
||||||
|
|||||||
Reference in New Issue
Block a user