Over Grouped Control
Status: ready
Symptom
The fix for Label Soup went too far: the whole cart row became one element. The noise is gone, and so is direct access to the quantity stepper and the Remove button.
Jasmine tea, $14, quantity 2, actions available
Changing the quantity now means opening the Actions rotor, if the action was added at all.
Where it shows up
Product cards, cart rows, settings rows, media controls, transaction lists, checkout summaries, appointment cells.
Why it blocks the task
The team measured swipes, not task completion. Fewer stops looks like progress until someone tries to change the quantity.
Switch Control scans faster but the stepper isn't a target anymore. Voice Control can't find "Remove" because it's not a control on screen. For a primary action, a hidden custom action is a dead end for anyone who doesn't know the Actions rotor.
Root cause
Grouping was applied as a blanket fix.
.combineor.ignoreon the whole row.- Interactive children became part of one parent element.
- A frequent action was moved into a custom action.
- The check counted stops instead of completing the task.
Before
CartRow(item)
.accessibilityElement(children: .ignore)
.accessibilityLabel(item.name)
.accessibilityValue("Quantity \(item.quantity), \(item.price)")
.accessibilityAction(named: "Increase quantity") { item.quantity += 1 }
.accessibilityAction(named: "Remove") { remove(item) }
This isn't always wrong. It's wrong when changing the quantity is the main thing the user does on this screen, which in a cart it is.
Fix
Verdict: group the summary, keep the controls as controls.
Why: the user gets a short summary and direct access to the actions. Switch Control and Voice Control see real targets.
VStack(alignment: .leading) {
ItemSummary(item)
.accessibilityElement(children: .combine)
Stepper("Quantity: \(item.quantity)", value: $item.quantity, in: 1...99)
Button("Remove") { remove(item) }
.accessibilityLabel("Remove \(item.name)")
.accessibilityInputLabels(["Remove", "Remove \(item.name)"])
}
Three stops per row: summary, stepper, Remove. Before the over-grouping fix it was six or seven. After it, one with nothing reachable.
UIKit: the cell isn't an element itself, and the order is set explicitly. The summary is a UIAccessibilityElement with a frame:
cell.isAccessibilityElement = false
let summary = UIAccessibilityElement(accessibilityContainer: cell)
summary.accessibilityLabel = "Jasmine tea, $14"
summary.accessibilityFrameInContainerSpace = titleStack.frame
cell.accessibilityElements = [summary, quantityStepper, removeButton]
Check
Profile: VoiceOver
Task: change the quantity of jasmine tea to 3, then remove it
Expected: the summary is one stop
the stepper is the next stop, swipe up increases the quantity
Remove names the item
the Actions rotor isn't needed
Profile: Switch Control
Task: change the quantity
Expected: the stepper is a scan target
Remove is a scan target
WCAG mapping
2.1.1 Keyboard: every control is operable without a hidden gesture.2.4.3 Focus Order: focus reaches every control the task needs.4.1.2 Name, Role, Value: interactive children keep their role and state.